/* * Copyright 2008-2011, Clemens Zeidler * Copyright 2022-2025, Haiku, Inc. All rights reserved. * Distributed under the terms of the MIT License. * * Authors: * Stephan Aßmus * Augustin Cavalier * Adrien Destugues * Axel Dörfler * Fredrik Holmqvist * Michael Kanis * Sylvain Kerjean * Murai Takashi * PawanYr * Samuel Rodríguez Pérez * Clemens Zeidler */ #include "movement_maker.h" #include #include #include #include //#define TRACE_MOVEMENT_MAKER #ifdef TRACE_MOVEMENT_MAKER # include # include static int32 sFunctionDepth = -1; # define CALLED(x...) FunctionTracer _ft(debug_printf, this, __PRETTY_FUNCTION__, sFunctionDepth) # define TRACE(x...) do { BString _to; \ _to.Append(' ', (sFunctionDepth + 1) * 2); \ char _extra[1024]; \ sprintf(_extra, x); \ debug_printf("%p -> %s%s", this, _to.String(), _extra); \ } while (0) # define LOG_EVENT(text...) do {} while (0) # define LOG_ERROR(text...) TRACE(text) #else # define TRACE(x...) # define CALLED(x...) # define LOG_ERROR(x...) do { debug_printf(x); } while(0) # define LOG_EVENT(x...) #endif // magic constants #define SYN_WIDTH (4100) #define SYN_HEIGHT (3140) static int32 make_small(float value) { return (int32)truncf(value); } static inline bool two_fingers(const touchpad_movement* event) { // TODO: Replace fingerWith related conditions when drivers are adjusted. // They seem to be taken from specifics of Synaptics device driver values // for w where 0 means 2 fingers and 1 is means 3 or more fingers. return count_set_bits(event->fingers) == 2 // || event->nFingers == 2 || event->fingerWidth == 0; } static inline bool two_or_more_fingers(const touchpad_movement* event) { // TODO: Replace fingerWith related conditions when drivers are adjusted. // They seem to be taken from specifics of Synaptics device driver values // for w where 0 means 2 fingers and 1 is means 3 or more fingers. return count_set_bits(event->fingers) >= 2 // || event->nFingers >= 2 || event->fingerWidth == 0 || event->fingerWidth == 1; } static inline bool three_fingers(const touchpad_movement* event) { // TODO: Replace fingerWith related conditions when drivers are adjusted. // They seem to be taken from specifics of Synaptics device driver values // for w where 0 means 2 fingers and 1 is means 3 or more fingers. return count_set_bits(event->fingers) == 3 // || event->nFingers = 3 || event->fingerWidth == 1; // This is 3 or more fingers for Synaptic } static inline bool three_or_more_fingers(const touchpad_movement* event) { // TODO: Replace fingerWith related conditions when drivers are adjusted. // They seem to be taken from specifics of Synaptics device driver values // for w where 0 means 2 fingers and 1 is means 3 or more fingers. return count_set_bits(event->fingers) > 2 // || event->nFingers > 2 || event->fingerWidth == 1; } // #pragma mark - void MovementMaker::SetSettings(const touchpad_settings& settings) { CALLED(); fSettings = settings; } void MovementMaker::SetSpecs(const touchpad_specs& specs) { CALLED(); fSpecs = specs; fAreaWidth = fSpecs.areaEndX - fSpecs.areaStartX; fAreaHeight = fSpecs.areaEndY - fSpecs.areaStartY; // calibrated on the synaptics touchpad fSpeed = SYN_WIDTH / fAreaWidth; fSmallMovement = 3 / fSpeed; } void MovementMaker::StartNewMovment() { CALLED(); if (fSettings.scroll_xstepsize <= 0) fSettings.scroll_xstepsize = 1; if (fSettings.scroll_ystepsize <= 0) fSettings.scroll_ystepsize = 1; fMovementMakerStarted = true; scrolling_x = 0; scrolling_y = 0; } void MovementMaker::GetMovement(uint32 posX, uint32 posY) { CALLED(); _GetRawMovement(posX, posY); } void MovementMaker::GetScrolling(uint32 posX, uint32 posY, bool reverse) { CALLED(); int32 stepsX = 0, stepsY = 0; int32 directionMultiplier = reverse ? -1 : 1; _GetRawMovement(posX, posY); _ComputeAcceleration(fSettings.scroll_acceleration); if (fSettings.scroll_xstepsize > 0) { scrolling_x += directionMultiplier * xDelta; stepsX = make_small(scrolling_x / fSettings.scroll_xstepsize); scrolling_x -= stepsX * fSettings.scroll_xstepsize; xDelta = stepsX; } else { scrolling_x = 0; xDelta = 0; } if (fSettings.scroll_ystepsize > 0) { scrolling_y += directionMultiplier * yDelta; stepsY = make_small(scrolling_y / fSettings.scroll_ystepsize); scrolling_y -= stepsY * fSettings.scroll_ystepsize; yDelta = -1 * stepsY; } else { scrolling_y = 0; yDelta = 0; } } void MovementMaker::_GetRawMovement(uint32 posX, uint32 posY) { CALLED(); // calibrated on the synaptics touchpad posX = posX * SYN_WIDTH / fAreaWidth; posY = posY * SYN_HEIGHT / fAreaHeight; const float acceleration = 0.8; const float translation = 12.0; int diff; if (fMovementMakerStarted) { fMovementMakerStarted = false; // init delta tracking fPreviousX = posX; fPreviousY = posY; // deltas are automatically reset } // accumulate delta and store current pos, reset if pos did not change diff = posX - fPreviousX; // lessen the effect of small diffs if ((diff > -fSmallMovement && diff < -1) || (diff > 1 && diff < fSmallMovement)) { diff /= 2; } if (diff == 0) fDeltaSumX = 0; else fDeltaSumX += diff; diff = posY - fPreviousY; // lessen the effect of small diffs if ((diff > -fSmallMovement && diff < -1) || (diff > 1 && diff < fSmallMovement)) { diff /= 2; } if (diff == 0) fDeltaSumY = 0; else fDeltaSumY += diff; fPreviousX = posX; fPreviousY = posY; // compute current delta and reset accumulated delta if // abs() is greater than 1 xDelta = fDeltaSumX / translation; yDelta = fDeltaSumY / translation; if (xDelta > 1.0) { fDeltaSumX = 0.0; xDelta = 1.0 + (xDelta - 1.0) * acceleration; } else if (xDelta < -1.0) { fDeltaSumX = 0.0; xDelta = -1.0 + (xDelta + 1.0) * acceleration; } if (yDelta > 1.0) { fDeltaSumY = 0.0; yDelta = 1.0 + (yDelta - 1.0) * acceleration; } else if (yDelta < -1.0) { fDeltaSumY = 0.0; yDelta = -1.0 + (yDelta + 1.0) * acceleration; } xDelta = make_small(xDelta); yDelta = make_small(yDelta); } void MovementMaker::_ComputeAcceleration(int8 accel_factor) { CALLED(); // acceleration float acceleration = 1; if (accel_factor != 0) { acceleration = 1 + sqrtf(xDelta * xDelta + yDelta * yDelta) * accel_factor / 50.0; } xDelta = make_small(xDelta * acceleration); yDelta = make_small(yDelta * acceleration); } // #pragma mark - #define fTapTimeOUT 200000 TouchpadMovement::TouchpadMovement() { CALLED(); fMovementStarted = false; fScrollingStarted = false; fTapStarted = false; fValidEdgeMotion = false; fDoubleClick = false; } TouchpadMovement::~TouchpadMovement() { CALLED(); } status_t TouchpadMovement::EventToMovement(const touchpad_movement* _event, mouse_movement* movement, bigtime_t& repeatTimeout) { CALLED(); if (!movement) return B_ERROR; TRACE("TM_EVENT: b:0x%" B_PRIx8 " nf:%" B_PRId8 " f:0x%" B_PRIx8 " x:%" B_PRIu32 " y:%" B_PRIu32 " p:%" B_PRIu8 " w:%" B_PRIu8 "\n", _event->buttons, count_set_bits(_event->fingers), _event->fingers, _event->xPosition, _event->yPosition, _event->zPressure, _event->fingerWidth ); TRACE("TM_STATUS: b:0x%" B_PRIx8 " %c%c%c%c%c%c" " dx:%" B_PRId32 " dy:%" B_PRId32 " tcks:%" B_PRId32 " cks:%" B_PRId32 "\n", fButtonsState, fMovementStarted ? 'M' : 'm', fScrollingStarted ? 'S' : 's', fTapStarted ? 'T' : 't', fTapdragStarted ? 'D' : 'd', fValidEdgeMotion ? 'E' : 'e', fDoubleClick ? 'C' : 'c', fTapDeltaX, fTapDeltaY, fTapClicks, fClickCount ); movement->xdelta = 0; movement->ydelta = 0; movement->buttons = 0; movement->wheel_ydelta = 0; movement->wheel_xdelta = 0; movement->modifiers = 0; movement->clicks = 0; movement->timestamp = system_time(); touchpad_movement event2 = *_event; if (!_ClickFingerButtonEmulator(&event2)) _SoftwareButtonAreas(&event2); const touchpad_movement* event = &event2; if ((movement->timestamp - fTapTime) > fTapTimeOUT) { if (fTapStarted) TRACE("TouchpadMovement: tap gesture timed out\n"); fTapStarted = false; if (!fDoubleClick || (movement->timestamp - fTapTime) > 2 * fTapTimeOUT) { fTapClicks = 0; } } if (event->buttons || two_or_more_fingers(event)) { fTapClicks = 0; fTapdragStarted = false; fTapStarted = false; if ((fSettings.edge_motion & (B_EDGE_MOTION_ON_BUTTON_CLICK_MOVE | B_EDGE_MOTION_ON_BUTTON_CLICK_DRAG)) != 0) fValidEdgeMotion = false; } if (event->zPressure >= fSpecs.minPressure && event->zPressure < fSpecs.maxPressure && ((event->fingerWidth >= 4 && event->fingerWidth <= 7) || two_or_more_fingers(event)) && (event->xPosition != 0 || event->yPosition != 0)) { // The touch pad is in touch with at least one finger if (!_CheckScrollingToMovement(event, movement)) _MoveToMovement(event, movement); } else _NoTouchToMovement(event, movement); if (fTapdragStarted || fValidEdgeMotion) { // We want the current event to be repeated in 50ms if no other // events occur in the interim. repeatTimeout = 1000 * 50; } else repeatTimeout = B_INFINITE_TIMEOUT; return B_OK; } bool TouchpadMovement::_ClickFingerButtonEmulator(touchpad_movement *event) { CALLED(); if (event->buttons != 0) { // ClickFinger behaviour. // Simulate with 2 fingers click => right button click // Simulate with 3 fingers click => middle button click if (fSettings.finger_click && two_fingers(event)) { event->buttons = 2; return true; } else if (fSettings.finger_click && three_fingers(event)) { event->buttons = 4; return true; } } return false; } void TouchpadMovement::_SoftwareButtonAreas(touchpad_movement *event) { CALLED(); // Software button areas. // - Emulation of button areas as clickpads do not have separated physical buttons from the one // overlapping the touch area. // - Pretend the the same button is still pressed if the finger hasn't been released regardless // of its current possition. // TODO: // - Implement top button area. // - Implement trackpoint emulator in an area. // i.e.: small centered square on the top of the clickpad. // - Make the areas configurable from settings and Input preference app. // i.e.: to allow switch left and right buttons, etc. if (event->buttons != 0) { if (fSettings.software_button_areas) { if (fButtonsState == 0) { uint32 evaluatePositionX = (event->xPosition == 0) ? fPreviousX : event->xPosition; uint32 evaluatePositionY = (event->yPosition == 0) ? fPreviousY : event->yPosition; if (evaluatePositionY > 0 && evaluatePositionY < (uint32) fSpecs.areaEndY / 5) { #if 0 // 2 software buttons: // - Emulates right buttom hardcoded to half left side of the touchpad, // otherwise left button. if (evaluatePositionX > (uint32) fSpecs.areaEndX / 2) { event->buttons = 2; } else { event->buttons = 1; } #else // 3 software buttons where the the middle button uses 1/6 of the area. if (evaluatePositionX > (uint32) fSpecs.areaEndX / 12 * 7) { event->buttons = 2; } else if (evaluatePositionX > (uint32) fSpecs.areaEndX / 12 * 5) { // Middle button area smaller than left and right buttons' area. event->buttons = 4; } else { event->buttons = 1; } #endif } } else { event->buttons = fButtonsState; } } } } // in pixel per second const int32 kEdgeMotionSpeed = 200; bool TouchpadMovement::_EdgeMotion(const touchpad_movement *event, mouse_movement *movement, bool validStart) { CALLED(); float xdelta = 0; float ydelta = 0; bigtime_t time = system_time(); if (fLastEdgeMotion != 0) { xdelta = fRestEdgeMotion + kEdgeMotionSpeed * float(time - fLastEdgeMotion) / (1000 * 1000); fRestEdgeMotion = xdelta - int32(xdelta); ydelta = xdelta; } else { fRestEdgeMotion = 0; } bool inXEdge = false; bool inYEdge = false; if (int32(event->xPosition) < fSpecs.areaStartX + fSpecs.edgeMotionWidth) { inXEdge = true; xdelta *= -1; } else if (event->xPosition > uint16( fSpecs.areaEndX - fSpecs.edgeMotionWidth)) { inXEdge = true; } if (int32(event->yPosition) < fSpecs.areaStartY + fSpecs.edgeMotionWidth) { inYEdge = true; ydelta *= -1; } else if (event->yPosition > uint16( fSpecs.areaEndY - fSpecs.edgeMotionWidth)) { inYEdge = true; } // for a edge motion the drag has to be started in the middle of the pad // TODO: this is difficult to understand simplify the code if (inXEdge && validStart) movement->xdelta = make_small(xdelta); if (inYEdge && validStart) movement->ydelta = make_small(ydelta); if (!inXEdge && !inYEdge) fLastEdgeMotion = 0; else fLastEdgeMotion = time; if ((inXEdge || inYEdge) && !validStart) return false; return true; } /*! If a button has been clicked (movement->buttons must be set accordingly), this function updates the fClickCount, as well as the \a movement's clicks field. Also, it sets the button state from movement->buttons. */ void TouchpadMovement::_UpdateButtons(mouse_movement *movement) { CALLED(); // set click count correctly according to double click timeout if (movement->buttons != 0 && fButtonsState == 0) { if (fClickLastTime + click_speed > movement->timestamp) fClickCount++; else fClickCount = 1; fClickLastTime = movement->timestamp; } if (movement->buttons != 0) movement->clicks = fClickCount; fButtonsState = movement->buttons; } void TouchpadMovement::_NoTouchToMovement(const touchpad_movement *event, mouse_movement *movement) { CALLED(); uint32 buttons = event->buttons; if (fMovementStarted) TRACE("TouchpadMovement: no touch event\n"); fScrollingStarted = false; fMovementStarted = false; fLastEdgeMotion = 0; if (fTapdragStarted && (movement->timestamp - fTapTime) < fTapTimeOUT) { buttons = kLeftButton; } // if the movement stopped switch off the tap drag when timeout is expired if ((movement->timestamp - fTapTime) > fTapTimeOUT) { if (fTapdragStarted) TRACE("TouchpadMovement: tap drag gesture timed out\n"); fTapdragStarted = false; fValidEdgeMotion = false; } if (abs(fTapDeltaX) > 15 || abs(fTapDeltaY) > 15) { fTapStarted = false; fTapClicks = 0; } if (fTapStarted || fDoubleClick) { TRACE("TouchpadMovement: tap gesture\n"); fTapClicks++; if (fTapClicks > 1) { TRACE("TouchpadMovement: empty click\n"); buttons = kNoButton; fTapClicks = 0; fDoubleClick = true; } else { buttons = kLeftButton; fTapStarted = false; fTapdragStarted = true; fDoubleClick = false; } } movement->buttons = buttons; _UpdateButtons(movement); } void TouchpadMovement::_MoveToMovement(const touchpad_movement *event, mouse_movement *movement) { CALLED(); bool isStartOfMovement = false; float pressure = 0; TRACE("TouchpadMovement: movement event\n"); if (!fMovementStarted) { isStartOfMovement = true; fMovementStarted = true; StartNewMovment(); } GetMovement(event->xPosition, event->yPosition); movement->xdelta = make_small(xDelta); movement->ydelta = make_small(yDelta); // tap gesture fTapDeltaX += make_small(xDelta); fTapDeltaY += make_small(yDelta); if (fTapdragStarted) { movement->buttons = kLeftButton; movement->clicks = 0; if (fSettings.edge_motion & B_EDGE_MOTION_ON_TAP_DRAG || (event->buttons && (fSettings.edge_motion & B_EDGE_MOTION_ON_BUTTON_CLICK_DRAG))) fValidEdgeMotion = _EdgeMotion(event, movement, fValidEdgeMotion); TRACE("TouchpadMovement: tap drag\n"); } else { if (fSettings.edge_motion & B_EDGE_MOTION_ON_MOVE || (event->buttons && (fSettings.edge_motion & B_EDGE_MOTION_ON_BUTTON_CLICK_MOVE))) fValidEdgeMotion = _EdgeMotion(event, movement, fValidEdgeMotion); TRACE("TouchpadMovement: movement set buttons\n"); movement->buttons = event->buttons; } // use only a fraction of pressure range, the max pressure seems to be // to high pressure = 20 * (event->zPressure - fSpecs.minPressure) / (fSpecs.realMaxPressure - fSpecs.minPressure); if (!fTapStarted && isStartOfMovement && fSettings.tapgesture_sensibility > 0. && fSettings.tapgesture_sensibility > (20 - pressure)) { TRACE("TouchpadMovement: tap started\n"); fTapStarted = true; fTapTime = system_time(); fTapDeltaX = 0; fTapDeltaY = 0; } _UpdateButtons(movement); } /*! Checks if this is a scrolling event or not, and also actually does the scrolling work if it is. \return \c true if this was a scrolling event, \c false if not. */ bool TouchpadMovement::_CheckScrollingToMovement(const touchpad_movement *event, mouse_movement *movement) { CALLED(); bool isSideScrollingV = false; bool isSideScrollingH = false; // if a button is pressed don't allow to scroll if (event->buttons != 0) return false; if ((fSpecs.areaEndX - fAreaWidth * fSettings.scroll_rightrange < event->xPosition && !fMovementStarted && fSettings.scroll_rightrange > 0.000001) || fSettings.scroll_rightrange > 0.999999) { isSideScrollingV = true; } if ((fSpecs.areaStartY + fAreaHeight * fSettings.scroll_bottomrange > event->yPosition && !fMovementStarted && fSettings.scroll_bottomrange > 0.000001) || fSettings.scroll_bottomrange > 0.999999) { isSideScrollingH = true; } bool isTwoFingerScrolling = two_fingers(event) && fSettings.scroll_twofinger; if (isTwoFingerScrolling) { // two finger scrolling is enabled isSideScrollingV = true; isSideScrollingH = fSettings.scroll_twofinger_horizontal; } if (!isSideScrollingV && !isSideScrollingH) { fScrollingStarted = false; return false; } TRACE("TouchpadMovement: scroll event\n"); fTapStarted = false; fTapClicks = 0; fTapdragStarted = false; fValidEdgeMotion = false; if (!fScrollingStarted) { fScrollingStarted = true; StartNewMovment(); } GetScrolling(event->xPosition, event->yPosition, isTwoFingerScrolling ? fSettings.scroll_twofinger_natural_scrolling : fSettings.scroll_reverse); movement->wheel_ydelta = make_small(yDelta); movement->wheel_xdelta = make_small(xDelta); if (isSideScrollingV && !isSideScrollingH) movement->wheel_xdelta = 0; else if (isSideScrollingH && !isSideScrollingV) movement->wheel_ydelta = 0; fButtonsState = movement->buttons; return true; }