xref: /haiku/src/add-ons/input_server/devices/mouse/movement_maker.cpp (revision 97f11716bfaa0f385eb0e28a52bf56a5023b9e99)
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