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
27static int32
28make_small(float value)
29{
30	if (value > 0)
31		return (int32)floorf(value);
32	else
33		return (int32)ceilf(value);
34}
35
36
37void
38MovementMaker::SetSettings(const touchpad_settings& settings)
39{
40	fSettings = settings;
41}
42
43
44void
45MovementMaker::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
58void
59MovementMaker::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
72void
73MovementMaker::GetMovement(uint32 posX, uint32 posY)
74{
75	_GetRawMovement(posX, posY);
76}
77
78
79void
80MovementMaker::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
112void
113MovementMaker::_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
183void
184MovementMaker::_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
204TouchpadMovement::TouchpadMovement()
205{
206	fMovementStarted = false;
207	fScrollingStarted = false;
208	fTapStarted = false;
209	fValidEdgeMotion = false;
210	fDoubleClick = false;
211}
212
213
214status_t
215TouchpadMovement::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
271const int32 kEdgeMotionSpeed = 200;
272
273
274bool
275TouchpadMovement::_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*/
334void
335TouchpadMovement::_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
354void
355TouchpadMovement::_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
407void
408TouchpadMovement::_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*/
464bool
465TouchpadMovement::_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