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::ivec2 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 = Gesture{};
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 = Gesture{};
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 Gesture::Coord midCurr = {
79 static_cast<int32_t>(0.5f * (leftPointer.curr.position.x + rightPointer.curr.position.x)),
80 static_cast<int32_t>(0.5f * (leftPointer.curr.position.y + rightPointer.curr.position.y))
81 };
82
83 if (panEnable) {
84 // Create the Pan event, but don't set it yet. Do that when the pointer moves.
85 currPan = Gesture{};
86 currPan->id = nextGestureId();
87 currPan->kind = Gesture::Kind::Pan;
88 currPan->state = Gesture::State::Started;
89 currPan->pan.midStartCoord = midCurr;
90 currPan->pan.midCurrCoord = midCurr;
91 }
92
93 if (rotateEnable) {
94 // Create the Rotate event, but don't set it yet. Do that when the pointer moves.
95 currRotate = Gesture{};
96 currRotate->id = nextGestureId();
97 currRotate->kind = Gesture::Kind::Rotate;
98 currRotate->state = Gesture::State::Started;
99 currRotate->rotate.center = midCurr;
100 currRotate->rotate.angle = 0;
101 }
102
103 if (pinchEnable) {
104 // Create the Pinch event, but don't set it yet. Do that when the pointer moves.
105 currPinch = Gesture{};
106 currPinch->id = nextGestureId();
107 currPinch->kind = Gesture::Kind::Pinch;
108 currPinch->state = Gesture::State::Started;
109 currPinch->pinch.center = midCurr;
110 currPinch->pinch.scale = 1;
111 }
112 }
113}
114
115void Cogs::Gestures::pointerUp(PointerType pointerType, PointerId pointerId, MouseButton button, glm::ivec2 position, double timestamp_ms)
116{
117 currPointerType = pointerType;
118 auto pointerIt = currentPointers.find(pointerId);
119 if (pointerIt == currentPointers.end()) {
120 LOG_WARNING(logger, "Discarding pointer release event from unknown pointer.");
121 return;
122 }
123
124 // Record the released pointer state.
125 PointerState& pointer = pointerIt->second;
126 pointer.curr.timestamp_ms = timestamp_ms;
127 pointer.curr.position.x = position.x;
128 pointer.curr.position.y = position.y;
129 pointer.state = PointerState::Released;
130
131 // End or cancel gestures
132 if (currDrag) {
133 // Ideally we calculate dx, dy, dt between curr and prev, but the last touch event is usually duplicated.
134 float dx = static_cast<float>(pointer.curr.position.x - pointer.init.position.x);
135 float dy = static_cast<float>(pointer.curr.position.y - pointer.init.position.y);
136 float dt = static_cast<float>(pointer.curr.timestamp_ms - pointer.init.timestamp_ms);
137 float distance = std::sqrt(dx * dx + dy * dy);
138 float velocity = 1000.f * distance / dt;
139 float velocityTreshold = displayScale * (pointerType == PointerType::Mouse ? mouseVelocityThreshold : touchVelocityThreshold);
140
141 // Check if the drag is actually a swipe. In that case, cancel the drag and submit the swipe.
142 if (swipeEnable && (velocityTreshold < velocity) && (timestamp_ms - pointer.init.timestamp_ms < swipeMaxDuration)) {
143
144 endUpdatingGesture(currDrag, Gesture::State::Cancelled);
145
146 Gesture swipe{};
147 swipe.id = nextGestureId();
148 swipe.kind = Gesture::Kind::Swipe;
149 swipe.state = Gesture::State::Ended;
150 swipe.swipe.button = button;
151 if (std::fabs(dx) > std::fabs(dy)) {
152 swipe.swipe.direction = dx < 0 ? Gesture::Swipe::Left : Gesture::Swipe::Right;
153 }
154 else {
155 swipe.swipe.direction = dy < 0 ? Gesture::Swipe::Up : Gesture::Swipe::Down;
156 }
157 swipe.swipe.startCoord = { pointer.init.position.x, pointer.init.position.y };
158 swipe.swipe.velocity = velocity;
159
160 currentGestures.push_back(std::move(swipe));
161 }
162 else if (currDrag->state == Gesture::State::Changed) {
163 // Otherwise, if we had an ongoing drag, submit it.
164 currDrag->state = Gesture::State::Ended;
165 currDrag->drag.currCoord = {pointer.curr.position.x, pointer.curr.position.y};
166 currentGestures.push_back(*currDrag);
167 currDrag = std::nullopt;
168 }
169 else {
170 // The pointer was pressed, but never moved. Discard the current drag gesture.
171 currDrag = std::nullopt;
172 }
173 }
174
175 // If the press was quick, count it as a tap and cancel the press gesture, otherwise end the press gesture normally.
176 if (currPress) {
177 if (timestamp_ms - pointer.init.timestamp_ms < tapMaxDuration) {
178 endUpdatingGesture(currPress, Gesture::State::Cancelled);
179
180 // If there was a tap less than doubleTapTreshold ago, submit this gesture as a double tap, otherwise submit a single tap.
181 // TODO: Don't submit taps when the finger was moved from the press prosition.
182 if (doubleTapEnable && timestamp_ms - lastTapTimestamp < doubleTapTreshold) {
183 Gesture dtap{};
184 dtap.id = nextGestureId();
185 dtap.kind = Gesture::Kind::DoubleTap;
186 dtap.state = Gesture::State::Ended;
187 dtap.doubleTap = { button, { pointer.curr.position.x, pointer.curr.position.y } };
188 currentGestures.push_back(std::move(dtap));
189 }
190 else {
191 lastTapTimestamp = timestamp_ms;
192
193 Gesture tap{};
194 tap.id = nextGestureId();
195 tap.kind = Gesture::Kind::Tap;
196 tap.state = Gesture::State::Ended;
197 tap.doubleTap = { button, { pointer.curr.position.x, pointer.curr.position.y } };
198 currentGestures.push_back(std::move(tap));
199 }
200 }
201 else {
202 currPress->press.coord = {pointer.curr.position.x, pointer.curr.position.y};
203 endUpdatingGesture(currPress, Gesture::State::Ended);
204 }
205 }
206
207 // End two finger gestures
208 endUpdatingGesture(currPan, Gesture::State::Ended);
209 endUpdatingGesture(currRotate, Gesture::State::Ended);
210 endUpdatingGesture(currPinch, Gesture::State::Ended);
211
212 // Pointer handled, now it can be removed
213 currentPointers.erase(pointerId);
214}
215
216void Cogs::Gestures::pointerMove(PointerType pointerType, PointerId pointerId, glm::ivec2 position, double timestamp_ms)
217{
218 currPointerType = pointerType;
219 auto pointerIt = currentPointers.find(pointerId);
220 if (pointerIt == currentPointers.end()) {
221 // Don't add the hovering pointer to currentPointers. Mouse specific.
222 if (hoverEnable) {
223 Gesture g{};
224 g.id = nextGestureId();
225 g.kind = Gesture::Kind::Hover;
226 g.state = Gesture::State::Ended;
227 g.hover = { { position.x, position.y } };
228 currentGestures.push_back(std::move(g));
229 }
230 }
231 else {
232 // Record the moved pointer state.
233 PointerState& pointer = pointerIt->second;
234 pointer.prev = pointer.curr;
235 pointer.curr.timestamp_ms = timestamp_ms;
236 pointer.curr.position = position;
237 pointer.state = PointerState::Moving;
238
239 // Send start events for gestures that crossed the treshold and send changed events for updating gestures.
240 bool isMouse = pointerType == PointerType::Mouse;
241 if (currentPointers.size() == 1) {
242 if (currDrag) {
243 float dx = static_cast<float>(pointer.curr.position.x - pointer.init.position.x);
244 float dy = static_cast<float>(pointer.curr.position.y - pointer.init.position.y);
245 float distanceSq = dx * dx + dy * dy;
246 float distanceTreshold = displayScale * (isMouse ? mouseMoveThreshold : touchMoveThreshold);
247
248 if (currDrag->state == Gesture::State::Started && distanceTreshold * distanceTreshold < distanceSq) {
249 // The pointer moved pass the distance treshold, cancel the press gesture and start the Drag one.
250 endUpdatingGesture(currPress, Gesture::State::Cancelled);
251
252 currentGestures.push_back(*currDrag);
253 currDrag->state = Gesture::State::Changed;
254 }
255 if (currDrag->state == Gesture::State::Changed &&
256 (currDrag->drag.currCoord.x != pointer.curr.position.x || currDrag->drag.currCoord.y != pointer.curr.position.y)) {
257 // Pointer moved, update the Drag gesture.
258 currDrag->drag.currCoord = { pointer.curr.position.x, pointer.curr.position.y };
259 currentGestures.push_back(*currDrag);
260 }
261 }
262 }
263 else if (currentPointers.size() == 2) {
264 const PointerState& leftPointer = currentPointers.begin()->second;
265 const PointerState& rightPointer = std::next(currentPointers.begin())->second;
266 float midCurrX = 0.5f * (leftPointer.curr.position.x + rightPointer.curr.position.x);
267 float midCurrY = 0.5f * (leftPointer.curr.position.y + rightPointer.curr.position.y);
268 Gesture::Coord midCurr = { static_cast<int32_t>(midCurrX), static_cast<int32_t>(midCurrY) };
269
270 if (currPan) {
271 float midInitX = 0.5f * (leftPointer.init.position.x + rightPointer.init.position.x);
272 float midInitY = 0.5f * (leftPointer.init.position.y + rightPointer.init.position.y);
273 float dx = midCurr.x - midInitX;
274 float dy = midCurr.y - midInitY;
275 float distanceSq = dx * dx + dy * dy;
276 float distanceTreshold = displayScale * (isMouse ? mouseMoveThreshold : touchMoveThreshold);
277
278 if (currPan->state == Gesture::State::Started && distanceTreshold * distanceTreshold < distanceSq) {
279 // The pointers moved pass the distance treshold, start the Pan gesture.
280 currentGestures.push_back(*currPan);
281 currPan->state = Gesture::State::Changed;
282 }
283 if (currPan->state == Gesture::State::Changed &&
284 (currPan->pan.midCurrCoord.x != midCurr.x || currPan->pan.midCurrCoord.y != midCurr.y)) {
285 // Pointer moved, update the Pan gesture.
286 currPan->pan.midCurrCoord = { midCurr.x, midCurr.y };
287 currentGestures.push_back(*currPan);
288 }
289 }
290
291 if (currRotate) {
292 glm::vec2 vi = glm::vec2(rightPointer.init.position - leftPointer.init.position);
293 glm::vec2 vc = glm::vec2(rightPointer.curr.position - leftPointer.curr.position);
294 float s = 1.f / (glm::length(vi) * glm::length(vc));
295 if (std::isfinite(s)) {
296 float angle_cos = s * glm::dot(vc, vi);
297 float angle_sin = s * glm::cross(glm::vec3(vc, 0.f), glm::vec3(vi, 0.f)).z;
298 float angle = std::atan2(angle_sin, angle_cos);
299 if (currRotate->state == Gesture::State::Started && std::abs(angle) > rotateThreshold) {
300 // The angle changed more than the rotation treshold, start the Rotation gesture.
301 currentGestures.push_back(*currRotate);
302 currRotate->state = Gesture::State::Changed;
303 }
304 if (currRotate->state == Gesture::State::Changed && currRotate->rotate.angle != angle) {
305 // Angle changed, update the Rotation gesture.
306 currRotate->rotate.center = { midCurr.x, midCurr.y };
307 currRotate->rotate.angle = angle;
308 currentGestures.push_back(*currRotate);
309 }
310 }
311 }
312
313 if (currPinch) {
314 glm::vec2 pi0(leftPointer.init.position);
315 glm::vec2 pi1(rightPointer.init.position);
316 glm::vec2 pc0(leftPointer.curr.position);
317 glm::vec2 pc1(rightPointer.curr.position);
318 float scale = glm::distance(pc1, pc0) / glm::distance(pi1, pi0);
319 if (std::isfinite(scale)) {
320 if (currPinch->state == Gesture::State::Started && std::abs(1.f - scale) > pinchThreshold) {
321 // The scale changed more than the treshold, start the Pinch gesture.
322 currentGestures.push_back(*currPinch);
323 currPinch->state = Gesture::State::Changed;
324 }
325 if (currPinch->state == Gesture::State::Changed && currPinch->pinch.scale != scale) {
326 // Scale changed, update the Pinch gesture.
327 currPinch->pinch.center = { midCurr.x, midCurr.y };
328 currPinch->pinch.scale = scale;
329 currentGestures.push_back(*currPinch);
330 }
331 }
332 }
333 }
334 }
335}
336
337void Cogs::Gestures::mouseWheelMove(int32_t delta) {
338 Gesture g{};
339 g.kind = Gesture::Kind::Wheel;
340 g.id = nextGestureId();
341 g.state = Gesture::State::Ended;
342 g.wheel.delta = delta;
343 currentGestures.push_back(std::move(g));
344}
345
346void Cogs::Gestures::reset() {
347 currentGestures = {};
348}
349
350void Cogs::Gestures::endUpdatingGesture(std::optional<Gesture>& gesture, Gesture::State state)
351{
352 if (gesture) {
353 // Submit end events only if the gesture started. For updating gestures, that means at least one Changed event.
354 if(gesture->kind == Gesture::Kind::Press || gesture->state == Gesture::State::Changed) {
355 gesture->state = state;
356 currentGestures.push_back(*gesture);
357 }
358 gesture = std::nullopt;
359 }
360}
361
363{
364 return currPointerType;
365}
PointerType getPointerType()
Get the type of pointer that was last registered.
Definition: Gestures.cpp:362
std::unordered_map< PointerId, PointerState > currentPointers
Set of currently active pointers.
Definition: Gestures.h:209
Log implementation class.
Definition: LogManager.h:139
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
@ 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.