1 /*
2 * Copyright 2008-2011, Clemens Zeidler <haiku@clemens-zeidler.de>
3 * Copyright 2022, Haiku, Inc. All rights reserved.
4 * Distributed under the terms of the MIT License.
5 */
6 #include "movement_maker.h"
7
8 #include <stdlib.h>
9 #include <math.h>
10
11 #include <KernelExport.h>
12
13
14 //#define TRACE_MOVEMENT_MAKER
15 #ifdef TRACE_MOVEMENT_MAKER
16 # define TRACE(x...) dprintf(x)
17 #else
18 # define TRACE(x...)
19 #endif
20
21
22 // magic constants
23 #define SYN_WIDTH (4100)
24 #define SYN_HEIGHT (3140)
25
26
27 static int32
make_small(float value)28 make_small(float value)
29 {
30 if (value > 0)
31 return (int32)floorf(value);
32 else
33 return (int32)ceilf(value);
34 }
35
36
37 void
SetSettings(const touchpad_settings & settings)38 MovementMaker::SetSettings(const touchpad_settings& settings)
39 {
40 fSettings = settings;
41 }
42
43
44 void
SetSpecs(const touchpad_specs & specs)45 MovementMaker::SetSpecs(const touchpad_specs& specs)
46 {
47 fSpecs = specs;
48
49 fAreaWidth = fSpecs.areaEndX - fSpecs.areaStartX;
50 fAreaHeight = fSpecs.areaEndY - fSpecs.areaStartY;
51
52 // calibrated on the synaptics touchpad
53 fSpeed = SYN_WIDTH / fAreaWidth;
54 fSmallMovement = 3 / fSpeed;
55 }
56
57
58 void
StartNewMovment()59 MovementMaker::StartNewMovment()
60 {
61 if (fSettings.scroll_xstepsize <= 0)
62 fSettings.scroll_xstepsize = 1;
63 if (fSettings.scroll_ystepsize <= 0)
64 fSettings.scroll_ystepsize = 1;
65
66 fMovementMakerStarted = true;
67 scrolling_x = 0;
68 scrolling_y = 0;
69 }
70
71
72 void
GetMovement(uint32 posX,uint32 posY)73 MovementMaker::GetMovement(uint32 posX, uint32 posY)
74 {
75 _GetRawMovement(posX, posY);
76 }
77
78
79 void
GetScrolling(uint32 posX,uint32 posY)80 MovementMaker::GetScrolling(uint32 posX, uint32 posY)
81 {
82 int32 stepsX = 0, stepsY = 0;
83
84 _GetRawMovement(posX, posY);
85 _ComputeAcceleration(fSettings.scroll_acceleration);
86
87 if (fSettings.scroll_xstepsize > 0) {
88 scrolling_x += xDelta;
89
90 stepsX = make_small(scrolling_x / fSettings.scroll_xstepsize);
91
92 scrolling_x -= stepsX * fSettings.scroll_xstepsize;
93 xDelta = stepsX;
94 } else {
95 scrolling_x = 0;
96 xDelta = 0;
97 }
98 if (fSettings.scroll_ystepsize > 0) {
99 scrolling_y += yDelta;
100
101 stepsY = make_small(scrolling_y / fSettings.scroll_ystepsize);
102
103 scrolling_y -= stepsY * fSettings.scroll_ystepsize;
104 yDelta = -1 * stepsY;
105 } else {
106 scrolling_y = 0;
107 yDelta = 0;
108 }
109 }
110
111
112 void
_GetRawMovement(uint32 posX,uint32 posY)113 MovementMaker::_GetRawMovement(uint32 posX, uint32 posY)
114 {
115 // calibrated on the synaptics touchpad
116 posX = posX * SYN_WIDTH / fAreaWidth;
117 posY = posY * SYN_HEIGHT / fAreaHeight;
118
119 const float acceleration = 0.8;
120 const float translation = 12.0;
121
122 int diff;
123
124 if (fMovementMakerStarted) {
125 fMovementMakerStarted = false;
126 // init delta tracking
127 fPreviousX = posX;
128 fPreviousY = posY;
129 // deltas are automatically reset
130 }
131
132 // accumulate delta and store current pos, reset if pos did not change
133 diff = posX - fPreviousX;
134 // lessen the effect of small diffs
135 if ((diff > -fSmallMovement && diff < -1)
136 || (diff > 1 && diff < fSmallMovement)) {
137 diff /= 2;
138 }
139 if (diff == 0)
140 fDeltaSumX = 0;
141 else
142 fDeltaSumX += diff;
143
144 diff = posY - fPreviousY;
145 // lessen the effect of small diffs
146 if ((diff > -fSmallMovement && diff < -1)
147 || (diff > 1 && diff < fSmallMovement)) {
148 diff /= 2;
149 }
150 if (diff == 0)
151 fDeltaSumY = 0;
152 else
153 fDeltaSumY += diff;
154
155 fPreviousX = posX;
156 fPreviousY = posY;
157
158 // compute current delta and reset accumulated delta if
159 // abs() is greater than 1
160 xDelta = fDeltaSumX / translation;
161 yDelta = fDeltaSumY / translation;
162 if (xDelta > 1.0) {
163 fDeltaSumX = 0.0;
164 xDelta = 1.0 + (xDelta - 1.0) * acceleration;
165 } else if (xDelta < -1.0) {
166 fDeltaSumX = 0.0;
167 xDelta = -1.0 + (xDelta + 1.0) * acceleration;
168 }
169
170 if (yDelta > 1.0) {
171 fDeltaSumY = 0.0;
172 yDelta = 1.0 + (yDelta - 1.0) * acceleration;
173 } else if (yDelta < -1.0) {
174 fDeltaSumY = 0.0;
175 yDelta = -1.0 + (yDelta + 1.0) * acceleration;
176 }
177
178 xDelta = make_small(xDelta);
179 yDelta = make_small(yDelta);
180 }
181
182
183 void
_ComputeAcceleration(int8 accel_factor)184 MovementMaker::_ComputeAcceleration(int8 accel_factor)
185 {
186 // acceleration
187 float acceleration = 1;
188 if (accel_factor != 0) {
189 acceleration = 1 + sqrtf(xDelta * xDelta
190 + yDelta * yDelta) * accel_factor / 50.0;
191 }
192
193 xDelta = make_small(xDelta * acceleration);
194 yDelta = make_small(yDelta * acceleration);
195 }
196
197
198 // #pragma mark -
199
200
201 #define fTapTimeOUT 200000
202
203
TouchpadMovement()204 TouchpadMovement::TouchpadMovement()
205 {
206 fMovementStarted = false;
207 fScrollingStarted = false;
208 fTapStarted = false;
209 fValidEdgeMotion = false;
210 fDoubleClick = false;
211 }
212
213
214 status_t
EventToMovement(const touchpad_movement * event,mouse_movement * movement,bigtime_t & repeatTimeout)215 TouchpadMovement::EventToMovement(const touchpad_movement* event, mouse_movement* movement,
216 bigtime_t& repeatTimeout)
217 {
218 if (!movement)
219 return B_ERROR;
220
221 movement->xdelta = 0;
222 movement->ydelta = 0;
223 movement->buttons = 0;
224 movement->wheel_ydelta = 0;
225 movement->wheel_xdelta = 0;
226 movement->modifiers = 0;
227 movement->clicks = 0;
228 movement->timestamp = system_time();
229
230 if ((movement->timestamp - fTapTime) > fTapTimeOUT) {
231 if (fTapStarted)
232 TRACE("TouchpadMovement: tap gesture timed out\n");
233 fTapStarted = false;
234 if (!fDoubleClick
235 || (movement->timestamp - fTapTime) > 2 * fTapTimeOUT) {
236 fTapClicks = 0;
237 }
238 }
239
240 if (event->buttons & kLeftButton) {
241 fTapClicks = 0;
242 fTapdragStarted = false;
243 fTapStarted = false;
244 fValidEdgeMotion = false;
245 }
246
247 if (event->zPressure >= fSpecs.minPressure
248 && event->zPressure < fSpecs.maxPressure
249 && ((event->fingerWidth >= 4 && event->fingerWidth <= 7)
250 || event->fingerWidth == 0 || event->fingerWidth == 1)
251 && (event->xPosition != 0 || event->yPosition != 0)) {
252 // The touch pad is in touch with at least one finger
253 if (!_CheckScrollingToMovement(event, movement))
254 _MoveToMovement(event, movement);
255 } else
256 _NoTouchToMovement(event, movement);
257
258
259 if (fTapdragStarted || fValidEdgeMotion) {
260 // We want the current event to be repeated in 50ms if no other
261 // events occur in the interim.
262 repeatTimeout = 1000 * 50;
263 } else
264 repeatTimeout = B_INFINITE_TIMEOUT;
265
266 return B_OK;
267 }
268
269
270 // in pixel per second
271 const int32 kEdgeMotionSpeed = 200;
272
273
274 bool
_EdgeMotion(const touchpad_movement * event,mouse_movement * movement,bool validStart)275 TouchpadMovement::_EdgeMotion(const touchpad_movement *event, mouse_movement *movement,
276 bool validStart)
277 {
278 float xdelta = 0;
279 float ydelta = 0;
280
281 bigtime_t time = system_time();
282 if (fLastEdgeMotion != 0) {
283 xdelta = fRestEdgeMotion + kEdgeMotionSpeed *
284 float(time - fLastEdgeMotion) / (1000 * 1000);
285 fRestEdgeMotion = xdelta - int32(xdelta);
286 ydelta = xdelta;
287 } else {
288 fRestEdgeMotion = 0;
289 }
290
291 bool inXEdge = false;
292 bool inYEdge = false;
293
294 if (int32(event->xPosition) < fSpecs.areaStartX + fSpecs.edgeMotionWidth) {
295 inXEdge = true;
296 xdelta *= -1;
297 } else if (event->xPosition > uint16(
298 fSpecs.areaEndX - fSpecs.edgeMotionWidth)) {
299 inXEdge = true;
300 }
301
302 if (int32(event->yPosition) < fSpecs.areaStartY + fSpecs.edgeMotionWidth) {
303 inYEdge = true;
304 ydelta *= -1;
305 } else if (event->yPosition > uint16(
306 fSpecs.areaEndY - fSpecs.edgeMotionWidth)) {
307 inYEdge = true;
308 }
309
310 // for a edge motion the drag has to be started in the middle of the pad
311 // TODO: this is difficult to understand simplify the code
312 if (inXEdge && validStart)
313 movement->xdelta = make_small(xdelta);
314 if (inYEdge && validStart)
315 movement->ydelta = make_small(ydelta);
316
317 if (!inXEdge && !inYEdge)
318 fLastEdgeMotion = 0;
319 else
320 fLastEdgeMotion = time;
321
322 if ((inXEdge || inYEdge) && !validStart)
323 return false;
324
325 return true;
326 }
327
328
329 /*! If a button has been clicked (movement->buttons must be set accordingly),
330 this function updates the fClickCount, as well as the
331 \a movement's clicks field.
332 Also, it sets the button state from movement->buttons.
333 */
334 void
_UpdateButtons(mouse_movement * movement)335 TouchpadMovement::_UpdateButtons(mouse_movement *movement)
336 {
337 // set click count correctly according to double click timeout
338 if (movement->buttons != 0 && fButtonsState == 0) {
339 if (fClickLastTime + click_speed > movement->timestamp)
340 fClickCount++;
341 else
342 fClickCount = 1;
343
344 fClickLastTime = movement->timestamp;
345 }
346
347 if (movement->buttons != 0)
348 movement->clicks = fClickCount;
349
350 fButtonsState = movement->buttons;
351 }
352
353
354 void
_NoTouchToMovement(const touchpad_movement * event,mouse_movement * movement)355 TouchpadMovement::_NoTouchToMovement(const touchpad_movement *event,
356 mouse_movement *movement)
357 {
358 uint32 buttons = event->buttons;
359
360 if (fMovementStarted)
361 TRACE("TouchpadMovement: no touch event\n");
362
363 fScrollingStarted = false;
364 fMovementStarted = false;
365 fLastEdgeMotion = 0;
366
367 if (fTapdragStarted
368 && (movement->timestamp - fTapTime) < fTapTimeOUT) {
369 buttons = kLeftButton;
370 }
371
372 // if the movement stopped switch off the tap drag when timeout is expired
373 if ((movement->timestamp - fTapTime) > fTapTimeOUT) {
374 if (fTapdragStarted)
375 TRACE("TouchpadMovement: tap drag gesture timed out\n");
376 fTapdragStarted = false;
377 fValidEdgeMotion = false;
378 }
379
380 if (abs(fTapDeltaX) > 15 || abs(fTapDeltaY) > 15) {
381 fTapStarted = false;
382 fTapClicks = 0;
383 }
384
385 if (fTapStarted || fDoubleClick) {
386 TRACE("TouchpadMovement: tap gesture\n");
387 fTapClicks++;
388
389 if (fTapClicks > 1) {
390 TRACE("TouchpadMovement: empty click\n");
391 buttons = kNoButton;
392 fTapClicks = 0;
393 fDoubleClick = true;
394 } else {
395 buttons = kLeftButton;
396 fTapStarted = false;
397 fTapdragStarted = true;
398 fDoubleClick = false;
399 }
400 }
401
402 movement->buttons = buttons;
403 _UpdateButtons(movement);
404 }
405
406
407 void
_MoveToMovement(const touchpad_movement * event,mouse_movement * movement)408 TouchpadMovement::_MoveToMovement(const touchpad_movement *event, mouse_movement *movement)
409 {
410 bool isStartOfMovement = false;
411 float pressure = 0;
412
413 TRACE("TouchpadMovement: movement event\n");
414 if (!fMovementStarted) {
415 isStartOfMovement = true;
416 fMovementStarted = true;
417 StartNewMovment();
418 }
419
420 GetMovement(event->xPosition, event->yPosition);
421
422 movement->xdelta = make_small(xDelta);
423 movement->ydelta = make_small(yDelta);
424
425 // tap gesture
426 fTapDeltaX += make_small(xDelta);
427 fTapDeltaY += make_small(yDelta);
428
429 if (fTapdragStarted) {
430 movement->buttons = kLeftButton;
431 movement->clicks = 0;
432
433 fValidEdgeMotion = _EdgeMotion(event, movement, fValidEdgeMotion);
434 TRACE("TouchpadMovement: tap drag\n");
435 } else {
436 TRACE("TouchpadMovement: movement set buttons\n");
437 movement->buttons = event->buttons;
438 }
439
440 // use only a fraction of pressure range, the max pressure seems to be
441 // to high
442 pressure = 20 * (event->zPressure - fSpecs.minPressure)
443 / (fSpecs.realMaxPressure - fSpecs.minPressure);
444 if (!fTapStarted
445 && isStartOfMovement
446 && fSettings.tapgesture_sensibility > 0.
447 && fSettings.tapgesture_sensibility > (20 - pressure)) {
448 TRACE("TouchpadMovement: tap started\n");
449 fTapStarted = true;
450 fTapTime = system_time();
451 fTapDeltaX = 0;
452 fTapDeltaY = 0;
453 }
454
455 _UpdateButtons(movement);
456 }
457
458
459 /*! Checks if this is a scrolling event or not, and also actually does the
460 scrolling work if it is.
461
462 \return \c true if this was a scrolling event, \c false if not.
463 */
464 bool
_CheckScrollingToMovement(const touchpad_movement * event,mouse_movement * movement)465 TouchpadMovement::_CheckScrollingToMovement(const touchpad_movement *event,
466 mouse_movement *movement)
467 {
468 bool isSideScrollingV = false;
469 bool isSideScrollingH = false;
470
471 // if a button is pressed don't allow to scroll, we likely be in a drag
472 // action
473 if (fButtonsState != 0)
474 return false;
475
476 if ((fSpecs.areaEndX - fAreaWidth * fSettings.scroll_rightrange
477 < event->xPosition && !fMovementStarted
478 && fSettings.scroll_rightrange > 0.000001)
479 || fSettings.scroll_rightrange > 0.999999) {
480 isSideScrollingV = true;
481 }
482 if ((fSpecs.areaStartY + fAreaHeight * fSettings.scroll_bottomrange
483 > event->yPosition && !fMovementStarted
484 && fSettings.scroll_bottomrange > 0.000001)
485 || fSettings.scroll_bottomrange > 0.999999) {
486 isSideScrollingH = true;
487 }
488 if ((event->fingerWidth == 0 || event->fingerWidth == 1)
489 && fSettings.scroll_twofinger) {
490 // two finger scrolling is enabled
491 isSideScrollingV = true;
492 isSideScrollingH = fSettings.scroll_twofinger_horizontal;
493 }
494
495 if (!isSideScrollingV && !isSideScrollingH) {
496 fScrollingStarted = false;
497 return false;
498 }
499
500 TRACE("TouchpadMovement: scroll event\n");
501
502 fTapStarted = false;
503 fTapClicks = 0;
504 fTapdragStarted = false;
505 fValidEdgeMotion = false;
506 if (!fScrollingStarted) {
507 fScrollingStarted = true;
508 StartNewMovment();
509 }
510 GetScrolling(event->xPosition, event->yPosition);
511 movement->wheel_ydelta = make_small(yDelta);
512 movement->wheel_xdelta = make_small(xDelta);
513
514 if (isSideScrollingV && !isSideScrollingH)
515 movement->wheel_xdelta = 0;
516 else if (isSideScrollingH && !isSideScrollingV)
517 movement->wheel_ydelta = 0;
518
519 fButtonsState = movement->buttons;
520
521 return true;
522 }
523