gnome-shell/js/ui/swipeTracker.js

784 lines
26 KiB
JavaScript
Raw Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import * as Main from './main.js';
import * as Params from '../misc/params.js';
// 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;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
// Derivative of easeOutCubic at t=0
const DURATION_MULTIPLIER = 3;
const ANIMATION_BASE_VELOCITY = 0.002;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
const EPSILON = 0.005;
const GESTURE_FINGER_COUNT = 3;
/** @enum {number} */
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;
}
}
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
const vertical = this.orientation === Clutter.Orientation.VERTICAL;
let delta = vertical ? dy : dx;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
this.emit('update', time, delta, distance);
break;
case Clutter.TouchpadGesturePhase.END:
case Clutter.TouchpadGesturePhase.CANCEL:
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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;
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();
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
this.emit('update', time, -delta, this._distance);
return true;
}
vfunc_gesture_end(_actor) {
let time = this.get_last_event(0).get_time();
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
this.emit('end', time, this._distance);
}
vfunc_gesture_cancel(_actor) {
let time = Clutter.get_current_event_time();
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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) {
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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;
}
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
const delta = (vertical ? dy : dx) * SCROLL_MULTIPLIER;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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 */
export const 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));
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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));
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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));
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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));
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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:
* This function can be used to combine swipe gesture and mouse
* scrolling.
*
* @param {Clutter.Event} scrollEvent an event to check
* @returns {boolean} whether the event can be handled by the tracker
*/
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);
}
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
_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]];
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
this._progress += delta / distance;
this._history.append(time, delta);
this._progress = Math.clamp(this._progress, ...this._getBounds(this._initialProgress));
this.emit('update', this._progress);
}
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
_getEndProgress(velocity, distance, isTouchpad) {
if (this._cancelled)
return this._cancelProgress;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
const threshold = isTouchpad ? VELOCITY_THRESHOLD_TOUCHPAD : VELOCITY_THRESHOLD_TOUCH;
if (Math.abs(velocity) < threshold)
return this._snapPoints[this._findClosestPoint(this._progress)];
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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 {
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
pos = Math.abs(velocity) * slope;
}
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
pos = pos * Math.sign(velocity) + this._progress;
pos = Math.clamp(pos, ...this._getBounds(this._initialProgress));
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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);
}
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
_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();
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
const endProgress = this._getEndProgress(velocity, distance, isTouchpad);
velocity /= distance;
if ((endProgress - this._progress) * velocity <= 0)
velocity = ANIMATION_BASE_VELOCITY;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
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);
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
if (duration > 0)
duration = Math.clamp(duration, MIN_ANIMATION_DURATION, maxDuration);
this._reset();
this.emit('end', duration, endProgress);
}
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
_cancelTouchGesture(_gesture, time, distance) {
if (this._state !== State.SCROLLING)
return;
this._cancelled = true;
swipeTracker: Rework end point calculation Previously we used a bunch of heuristics for this. We checked if velocity was directed towards the nearest snap point and its value was larger than a threshold. If it is, we completed the swipe, otherwise we cancelled it. This was good enough at the time, because this code was originally written for back/forward swipe. Since then, the swipe tracker was extended to handle arbitrary snap points and not just 0 and 1, or -1 and 0, depending on text direction. After that it was iterated on, but never significantly redone. This worked well enough, but had two problems: 1. In some cases, notably overview, it may be wanted to be able to swipe through multiple pages at once. This wasn't really possible because we always picked the adjacent snap point. 2. Since we can't do that well, we want to restrict swipes to one page at a time. It was done in a rather hacky way by clamping the position into [-1, 1] range from the place where we started the swipe. This works if we start the swipe from idle position, but if an animation was already going, the range would be clamped to arbitrary values, and very likely containing only one snap point, which we already swiped past at this point. In this case, finishing the swipe would cancel it regardless of velocity. This means that if one tries to quickly move through carousel pages via swiping, half of the swipes will be inexplicably cancelled. We'll use the deceleration formula from https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060#10ce to calculate then projection point, then pick the nearest snap point and calculate the duration as we did before. It works well enough for short distances, but has two problems: 1. It caps the maximum distance at a pretty low value - about 5 pages in my testing. 2. With how we pick the nearest snap point, it's too easy to accidentally cancel the swipe, To combat the first problem, we can modify the curve: only use linear function at small distances, and smoothly transition it to a parabola further. For the second problem we can add two special cases: first, if the swipe ended up between the initial snap point and the next one, we always prefer the latter. Second, a good old velocity threshold for cancelling. We'll also use a slightly smaller deceleration value for touchpad: 0.997 instead of 0.998. Now that we can pick any snap point, the [-1, 1] clamping doesn't make sense anymore, so instead let's replace it with a more flexible mechanism: if we're near a snap point, pick its adjacent snap points. Otherwise, take the two nearest snap points, and take their adjacent snap points. This way we have 3 snap points to choose from when starting a swipe from an idle position, and 4 if we start during an ongoing transition. This way, if we've just swiped from snap point n to n+1, the transition will pick snap points n-1, n, n+1, n+2 and if we swipe again, we will likely land on n+2. During that transition, if we swipe again, it will likely have already passed the snap point n+1, so this time the available snap points will be n, n+1, n+2, n+3, so we can swipe again and it will still complete, and so on. This will make it easy to allow multi-page swipes as well, by just removing the clamping. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1647>
2021-02-07 18:39:54 +00:00
this._endGesture(time, distance, false);
}
/**
* 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.
*
* `cancelProgress` must always be a snap point, or a value matching
* some other non-transient state.
*
* @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
*/
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;
}
}
});