Cogs.Core
Gestures.cpp
1#include "Gestures.h"
2
3#include "Timer.h"
4#include "../Logging/Logger.h"
5
6#include <glm/glm.hpp>
7
8namespace
9{
11}
12
13Cogs::Gestures::Gestures()
14{
15 // The current gestures require max 2 pointers, but let's be generous and preallocate space for 5.
16 currentPointers.reserve(5);
17}
18
19void Cogs::Gestures::update()
20{
21 for (const auto& [id, pointer] : currentPointers) {
22 // If a pressed pointer hasn't been updated in 10 seconds, assume something went wrong and discard it.
23 if (pointer.curr.timestamp_ms - pointer.prev.timestamp_ms > 10000.0) {
24 if (pointer.state == PointerState::Pressed || (pointer.state == PointerState::Moving && pointer.curr.position == pointer.prev.position)) {
25 LOG_WARNING(logger, "Discarding out-dated pointer id=%zu", id);
26 pointerUp(pointer.type, id, pointer.button, pointer.curr.position, static_cast<double>(Timer::currentTimeMilliseconds()));
27 }
28 }
29 }
30
31 presentedGestures = std::move(currentGestures);
32 currentGestures = {};
33}
34
35void Cogs::Gestures::pointerDown(PointerType pointerType, PointerId pointerId, MouseButton button, glm::vec2 position, double timestamp_ms)
36{
37 currPointerType = pointerType;
38 // Record the pressed pointer.
39 PointerState& pointer = currentPointers[pointerId];
40 pointer.type = pointerType;
41 pointer.button = button;
42 pointer.init.timestamp_ms = timestamp_ms;
43 pointer.init.position = position;
44 pointer.prev = pointer.init;
45 pointer.curr = pointer.init;
46 pointer.state = PointerState::Pressed;
47
48 // New pointer, cancel all updating gestures.
49 endUpdatingGesture(currPress, Gesture::State::Cancelled);
50 endUpdatingGesture(currDrag, Gesture::State::Cancelled);
51 endUpdatingGesture(currPan, Gesture::State::Cancelled);
52 endUpdatingGesture(currRotate, Gesture::State::Cancelled);
53 endUpdatingGesture(currPinch, Gesture::State::Cancelled);
54
55 size_t pointersCount = currentPointers.size();
56 if (pointersCount == 1) {
57 if (pressEnable) {
58 // Pressed one button, set the Press start event.
59 currPress.emplace();
60 currPress->id = nextGestureId();
61 currPress->kind = Gesture::Kind::Press;
62 currPress->state = Gesture::State::Started;
63 currPress->press = { button, {pointer.curr.position.x, pointer.curr.position.y} };
64 currentGestures.push_back(*currPress);
65 }
66 if (dragEnable) {
67 // Create the Drag event, but don't set it yet. Do that when the pointer moves.
68 currDrag.emplace();
69 currDrag->id = nextGestureId();
70 currDrag->kind = Gesture::Kind::Drag;
71 currDrag->state = Gesture::State::Started;
72 currDrag->drag = { button, {pointer.init.position.x, pointer.init.position.y}, {pointer.curr.position.x, pointer.curr.position.y} };
73 }
74 }
75 else if (pointersCount == 2) {
76 const PointerState& leftPointer = currentPointers.begin()->second;
77 const PointerState& rightPointer = std::next(currentPointers.begin())->second;
78 glm::vec2 midCurr(0.5f * (leftPointer.curr.position.x + rightPointer.curr.position.x),
79 0.5f * (leftPointer.curr.position.y + rightPointer.curr.position.y));
80
81 if (panEnable) {
82 // Create the Pan event, but don't set it yet. Do that when the pointer moves.
83 currPan.emplace();
84 currPan->id = nextGestureId();
85 currPan->kind = Gesture::Kind::Pan;
86 currPan->state = Gesture::State::Started;
87 currPan->pan.midStartCoord = midCurr;
88 currPan->pan.midCurrCoord = midCurr;
89 }
90
91 if (rotateEnable) {
92 // Create the Rotate event, but don't set it yet. Do that when the pointer moves.
93 currRotate.emplace();
94 currRotate->id = nextGestureId();
95 currRotate->kind = Gesture::Kind::Rotate;
96 currRotate->state = Gesture::State::Started;
97 currRotate->rotate.center = midCurr;
98 currRotate->rotate.angle = 0;
99 }
100
101 if (pinchEnable) {
102 // Create the Pinch event, but don't set it yet. Do that when the pointer moves.
103 currPinch.emplace();
104 currPinch->id = nextGestureId();
105 currPinch->kind = Gesture::Kind::Pinch;
106 currPinch->state = Gesture::State::Started;
107 currPinch->pinch.center = midCurr;
108 currPinch->pinch.scale = 1;
109 }
110 }
111}
112
113void Cogs::Gestures::pointerUp(PointerType pointerType, PointerId pointerId, MouseButton button, glm::vec2 position, double timestamp_ms)
114{
115 currPointerType = pointerType;
116 auto pointerIt = currentPointers.find(pointerId);
117 if (pointerIt == currentPointers.end()) {
118 LOG_WARNING(logger, "Discarding pointer release event from unknown pointer.");
119 return;
120 }
121
122 // Record the released pointer state.
123 PointerState& pointer = pointerIt->second;
124 pointer.curr.timestamp_ms = timestamp_ms;
125 pointer.curr.position.x = position.x;
126 pointer.curr.position.y = position.y;
127 pointer.state = PointerState::Released;
128
129 // End or cancel gestures
130 if (currDrag) {
131 // Ideally we calculate dx, dy, dt between curr and prev, but the last touch event is usually duplicated.
132 float dx = static_cast<float>(pointer.curr.position.x - pointer.init.position.x);
133 float dy = static_cast<float>(pointer.curr.position.y - pointer.init.position.y);
134 float dt = static_cast<float>(pointer.curr.timestamp_ms - pointer.init.timestamp_ms);
135 float distance = std::sqrt(dx * dx + dy * dy);
136 float velocity = 1000.f * distance / dt;
137 float velocityTreshold = displayScale * (pointerType == PointerType::Mouse ? mouseVelocityThreshold : touchVelocityThreshold);
138
139 // Check if the drag is actually a swipe. In that case, cancel the drag and submit the swipe.
140 if (swipeEnable && (velocityTreshold < velocity) && (timestamp_ms - pointer.init.timestamp_ms < swipeMaxDuration)) {
141
142 endUpdatingGesture(currDrag, Gesture::State::Cancelled);
143
144 Gesture swipe;
145 swipe.id = nextGestureId();
146 swipe.kind = Gesture::Kind::Swipe;
147 swipe.state = Gesture::State::Ended;
148 swipe.swipe.button = button;
149 if (std::fabs(dx) > std::fabs(dy)) {
150 swipe.swipe.direction = dx < 0 ? Gesture::Swipe::Left : Gesture::Swipe::Right;
151 }
152 else {
153 swipe.swipe.direction = dy < 0 ? Gesture::Swipe::Up : Gesture::Swipe::Down;
154 }
155 swipe.swipe.startCoord = { pointer.init.position.x, pointer.init.position.y };
156 swipe.swipe.velocity = velocity;
157
158 currentGestures.push_back(std::move(swipe));
159 }
160 else if (currDrag->state == Gesture::State::Changed) {
161 // Otherwise, if we had an ongoing drag, submit it.
162 currDrag->state = Gesture::State::Ended;
163 currDrag->drag.currCoord = {pointer.curr.position.x, pointer.curr.position.y};
164 currentGestures.push_back(*currDrag);
165 currDrag = std::nullopt;
166 }
167 else {
168 // The pointer was pressed, but never moved. Discard the current drag gesture.
169 currDrag = std::nullopt;
170 }
171 }
172
173 // If the press was quick, count it as a tap and cancel the press gesture, otherwise end the press gesture normally.
174 if (currPress) {
175 if (timestamp_ms - pointer.init.timestamp_ms < tapMaxDuration) {
176 endUpdatingGesture(currPress, Gesture::State::Cancelled);
177
178 // If there was a tap less than doubleTapTreshold ago, submit this gesture as a double tap, otherwise submit a single tap.
179 // TODO: Don't submit taps when the finger was moved from the press prosition.
180 if (doubleTapEnable && timestamp_ms - lastTapTimestamp < doubleTapTreshold) {
181 Gesture dtap{};
182 dtap.id = nextGestureId();
183 dtap.kind = Gesture::Kind::DoubleTap;
184 dtap.state = Gesture::State::Ended;
185 dtap.doubleTap = { button, { pointer.curr.position.x, pointer.curr.position.y } };
186 currentGestures.push_back(std::move(dtap));
187 }
188 else {
189 lastTapTimestamp = timestamp_ms;
190
191 Gesture tap{};
192 tap.id = nextGestureId();
193 tap.kind = Gesture::Kind::Tap;
194 tap.state = Gesture::State::Ended;
195 tap.doubleTap = { button, { pointer.curr.position.x, pointer.curr.position.y } };
196 currentGestures.push_back(std::move(tap));
197 }
198 }
199 else {
200 currPress->press.coord = {pointer.curr.position.x, pointer.curr.position.y};
201 endUpdatingGesture(currPress, Gesture::State::Ended);
202 }
203 }
204
205 // End two finger gestures
206 endUpdatingGesture(currPan, Gesture::State::Ended);
207 endUpdatingGesture(currRotate, Gesture::State::Ended);
208 endUpdatingGesture(currPinch, Gesture::State::Ended);
209
210 // Pointer handled, now it can be removed
211 currentPointers.erase(pointerId);
212}
213
214void Cogs::Gestures::pointerMove(PointerType pointerType, PointerId pointerId, glm::vec2 position, double timestamp_ms)
215{
216 currPointerType = pointerType;
217 auto pointerIt = currentPointers.find(pointerId);
218 if (pointerIt == currentPointers.end()) {
219 // Don't add the hovering pointer to currentPointers. Mouse specific.
220 if (hoverEnable) {
221 Gesture g{};
222 g.id = nextGestureId();
223 g.kind = Gesture::Kind::Hover;
224 g.state = Gesture::State::Ended;
225 g.hover = { { position.x, position.y } };
226 currentGestures.push_back(std::move(g));
227 }
228 }
229 else {
230 // Record the moved pointer state.
231 PointerState& pointer = pointerIt->second;
232 pointer.prev = pointer.curr;
233 pointer.curr.timestamp_ms = timestamp_ms;
234 pointer.curr.position = position;
235 pointer.state = PointerState::Moving;
236
237 // Send start events for gestures that crossed the treshold and send changed events for updating gestures.
238 bool isMouse = pointerType == PointerType::Mouse;
239 if (currentPointers.size() == 1) {
240 if (currDrag) {
241 float dx = static_cast<float>(pointer.curr.position.x - pointer.init.position.x);
242 float dy = static_cast<float>(pointer.curr.position.y - pointer.init.position.y);
243 float distanceSq = dx * dx + dy * dy;
244 float distanceTreshold = displayScale * (isMouse ? mouseMoveThreshold : touchMoveThreshold);
245
246 if (currDrag->state == Gesture::State::Started && distanceTreshold * distanceTreshold < distanceSq) {
247 // The pointer moved pass the distance treshold, cancel the press gesture and start the Drag one.
248 endUpdatingGesture(currPress, Gesture::State::Cancelled);
249
250 currentGestures.push_back(*currDrag);
251 currDrag->state = Gesture::State::Changed;
252 }
253 if (currDrag->state == Gesture::State::Changed &&
254 (currDrag->drag.currCoord.x != pointer.curr.position.x || currDrag->drag.currCoord.y != pointer.curr.position.y)) {
255 // Pointer moved, update the Drag gesture.
256 currDrag->drag.currCoord = { pointer.curr.position.x, pointer.curr.position.y };
257 currentGestures.push_back(*currDrag);
258 }
259 }
260 }
261 else if (currentPointers.size() == 2) {
262 const PointerState& leftPointer = currentPointers.begin()->second;
263 const PointerState& rightPointer = std::next(currentPointers.begin())->second;
264 float midCurrX = 0.5f * (leftPointer.curr.position.x + rightPointer.curr.position.x);
265 float midCurrY = 0.5f * (leftPointer.curr.position.y + rightPointer.curr.position.y);
266 glm::vec2 midCurr(midCurrX, midCurrY);
267
268 if (currPan) {
269 float midInitX = 0.5f * (leftPointer.init.position.x + rightPointer.init.position.x);
270 float midInitY = 0.5f * (leftPointer.init.position.y + rightPointer.init.position.y);
271 float dx = midCurr.x - midInitX;
272 float dy = midCurr.y - midInitY;
273 float distanceSq = dx * dx + dy * dy;
274 float distanceTreshold = displayScale * (isMouse ? mouseMoveThreshold : touchMoveThreshold);
275
276 if (currPan->state == Gesture::State::Started && distanceTreshold * distanceTreshold < distanceSq) {
277 // The pointers moved pass the distance treshold, start the Pan gesture.
278 currentGestures.push_back(*currPan);
279 currPan->state = Gesture::State::Changed;
280 }
281 if (currPan->state == Gesture::State::Changed &&
282 (currPan->pan.midCurrCoord.x != midCurr.x || currPan->pan.midCurrCoord.y != midCurr.y)) {
283 // Pointer moved, update the Pan gesture.
284 currPan->pan.midCurrCoord = { midCurr.x, midCurr.y };
285 currentGestures.push_back(*currPan);
286 }
287 }
288
289 if (currRotate) {
290 glm::vec2 vi = glm::vec2(rightPointer.init.position - leftPointer.init.position);
291 glm::vec2 vc = glm::vec2(rightPointer.curr.position - leftPointer.curr.position);
292 float s = 1.f / (glm::length(vi) * glm::length(vc));
293 if (std::isfinite(s)) {
294 float angle_cos = s * glm::dot(vc, vi);
295 float angle_sin = s * glm::cross(glm::vec3(vc, 0.f), glm::vec3(vi, 0.f)).z;
296 float angle = std::atan2(angle_sin, angle_cos);
297 if (currRotate->state == Gesture::State::Started && std::abs(angle) > rotateThreshold) {
298 // The angle changed more than the rotation treshold, start the Rotation gesture.
299 currentGestures.push_back(*currRotate);
300 currRotate->state = Gesture::State::Changed;
301 }
302 if (currRotate->state == Gesture::State::Changed && currRotate->rotate.angle != angle) {
303 // Angle changed, update the Rotation gesture.
304 currRotate->rotate.center = { midCurr.x, midCurr.y };
305 currRotate->rotate.angle = angle;
306 currentGestures.push_back(*currRotate);
307 }
308 }
309 }
310
311 if (currPinch) {
312 glm::vec2 pi0(leftPointer.init.position);
313 glm::vec2 pi1(rightPointer.init.position);
314 glm::vec2 pc0(leftPointer.curr.position);
315 glm::vec2 pc1(rightPointer.curr.position);
316 float scale = glm::distance(pc1, pc0) / glm::distance(pi1, pi0);
317 if (std::isfinite(scale)) {
318 if (currPinch->state == Gesture::State::Started && std::abs(1.f - scale) > pinchThreshold) {
319 // The scale changed more than the treshold, start the Pinch gesture.
320 currentGestures.push_back(*currPinch);
321 currPinch->state = Gesture::State::Changed;
322 }
323 if (currPinch->state == Gesture::State::Changed && currPinch->pinch.scale != scale) {
324 // Scale changed, update the Pinch gesture.
325 currPinch->pinch.center = { midCurr.x, midCurr.y };
326 currPinch->pinch.scale = scale;
327 currentGestures.push_back(*currPinch);
328 }
329 }
330 }
331 }
332 }
333}
334
335void Cogs::Gestures::mouseWheelMove(int32_t delta) {
336 Gesture g{};
337 g.kind = Gesture::Kind::Wheel;
338 g.id = nextGestureId();
339 g.state = Gesture::State::Ended;
340 g.wheel.delta = delta;
341 currentGestures.push_back(std::move(g));
342}
343
344void Cogs::Gestures::reset() {
345 currentGestures = {};
346}
347
348void Cogs::Gestures::endUpdatingGesture(std::optional<Gesture>& gesture, Gesture::State state)
349{
350 if (gesture) {
351 // Submit end events only if the gesture started. For updating gestures, that means at least one Changed event.
352 if(gesture->kind == Gesture::Kind::Press || gesture->state == Gesture::State::Changed) {
353 gesture->state = state;
354 currentGestures.push_back(*gesture);
355 }
356 gesture = std::nullopt;
357 }
358}
359
361{
362 return currPointerType;
363}
PointerType getPointerType()
Get the type of pointer that was last registered.
Definition: Gestures.cpp:360
std::unordered_map< PointerId, PointerState > currentPointers
Set of currently active pointers.
Definition: Gestures.h:208
Log implementation class.
Definition: LogManager.h:140
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:181
@ Press
A long press, a press that was too long to be a tap, events on press start and end.
@ Pinch
Two-finger movement where the two fingers comes closer or farther away, see pinchThreshold,...
@ Tap
A short press, see tapMaxDuration, single-fire event.
@ Swipe
Swift pointer movement with button pressed or touch, see swipeMaxDuration, mouseVelocityThreshold and...
@ Rotate
Two-finger rotation movement where fingers are roughly equidistant, see rotateThreshold,...
@ DoubleTap
Two taps in quick succession, see doubleTapTreshold, single-fire event.
@ Wheel
Movemement of the mouse wheel, single-fire event.
@ Drag
Pointer movement with a button pressed or touch, see mouseMoveThreshold and touchMoveThreshold,...
@ Hover
Mouse pointer hovers over window without any buttons pressed, single-fire event.
@ Pan
Two-finger touch movement where fingers are roughly equidistant, see mouseMoveThreshold and touchMove...
@ Ended
Gesture ended or single-fire events, used by all gestures.
@ Changed
Gesture changed, used by: Drag, Swipe, Pan, Rotate, Pinch.
@ Started
Gesture started, used by: Press, Drag, Swipe, Pan, Rotate, Pinch.
@ Cancelled
Gesture cancelled, used by: Press, Drag, Swipe, Pan, Rotate, Pinch.