Merge pull request #26166 from ksooo/improve-plugin-ctx-menus
[xbmc.git] / xbmc / input / InertialScrollingHandler.cpp
blob18b345b8951cafc22cf1cdcc3fbae63014e19476
1 /*
2 * Copyright (C) 2011-2024 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
9 #include "InertialScrollingHandler.h"
11 #include "ServiceBroker.h"
12 #include "application/Application.h"
13 #include "application/ApplicationComponents.h"
14 #include "application/ApplicationPowerHandling.h"
15 #include "guilib/GUIComponent.h"
16 #include "guilib/GUIWindowManager.h"
17 #include "input/actions/Action.h"
18 #include "input/actions/ActionIDs.h"
19 #include "input/touch/generic/GenericTouchInputHandler.h"
20 #include "utils/TimeUtils.h"
21 #include "windowing/WinSystem.h"
23 #include <cmath>
24 #include <numeric>
26 // time for reaching velocity 0 in secs
27 #define TIME_TO_ZERO_SPEED 1.0f
28 // minimum speed for doing inertial scroll is 100 pixels / s
29 #define MINIMUM_SPEED_FOR_INERTIA 200
30 // maximum speed for reducing time to zero
31 #define MAXIMUM_SPEED_FOR_REDUCTION 750
32 // maximum time between last movement and gesture end in ms to consider as moving
33 #define MAXIMUM_DELAY_FOR_INERTIA 200
35 CInertialScrollingHandler::CInertialScrollingHandler() : m_iLastGesturePoint(CPoint(0, 0))
39 unsigned int CInertialScrollingHandler::PanPoint::TimeElapsed() const
41 return CTimeUtils::GetFrameTime() - time;
44 bool CInertialScrollingHandler::CheckForInertialScrolling(const CAction* action)
46 bool ret = false; // return value - false no inertial scrolling - true - inertial scrolling
48 if (CServiceBroker::GetWinSystem()->HasInertialGestures())
50 return ret; // no need for emulating inertial scrolling - windowing does support it natively.
53 // reset screensaver during pan
54 if (action->GetID() == ACTION_GESTURE_PAN)
56 auto& components = CServiceBroker::GetAppComponents();
57 const auto appPower = components.GetComponent<CApplicationPowerHandling>();
58 if (appPower)
59 appPower->ResetScreenSaver();
60 if (!m_bScrolling)
62 m_panPoints.emplace_back(CTimeUtils::GetFrameTime(),
63 CVector{action->GetAmount(4), action->GetAmount(5)});
65 return false;
68 // mouse click aborts scrolling
69 if (m_bScrolling && action->GetID() == ACTION_MOUSE_LEFT_CLICK)
71 ret = true;
72 m_bAborting = true; // lets abort
75 // trim saved pan points to time range that qualifies for inertial scrolling
76 while (!m_panPoints.empty() && m_panPoints.front().TimeElapsed() > MAXIMUM_DELAY_FOR_INERTIA)
77 m_panPoints.pop_front();
79 // on begin/tap stop all inertial scrolling
80 if (action->GetID() == ACTION_GESTURE_BEGIN)
82 // release any former exclusive mouse mode
83 // for making switching between multiple lists
84 // possible
85 CGUIMessage message(GUI_MSG_EXCLUSIVE_MOUSE, 0, 0);
86 CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
87 m_bScrolling = false;
88 // wakeup screensaver on pan begin
89 auto& components = CServiceBroker::GetAppComponents();
90 const auto appPower = components.GetComponent<CApplicationPowerHandling>();
91 appPower->ResetScreenSaver();
92 appPower->WakeUpScreenSaverAndDPMS();
94 else if (action->GetID() == ACTION_GESTURE_END &&
95 !m_panPoints.empty()) // do we need to animate inertial scrolling?
97 // Calculate velocity in the last MAXIMUM_DELAY_FOR_INERTIA milliseconds.
98 // Do not use the velocity given by the ACTION_GESTURE_END data - it is calculated
99 // for the whole duration of the touch and thus useless for inertia. The user
100 // may scroll around for a few seconds and then only at the end flick in one
101 // direction. Only the last flick should be relevant here.
102 auto velocitySum =
103 std::accumulate(m_panPoints.cbegin(), m_panPoints.cend(), CVector{},
104 [](CVector val, PanPoint const& p) { return val + p.velocity; });
105 auto velocityX = velocitySum.x / m_panPoints.size();
106 auto velocityY = velocitySum.y / m_panPoints.size();
108 m_timeToZero = TIME_TO_ZERO_SPEED;
109 auto velocityMax = std::max(std::abs(velocityX), std::abs(velocityY));
110 #ifdef TARGET_DARWIN_OSX
111 float dpiScale = 1.0;
112 #else
113 float dpiScale = CGenericTouchInputHandler::GetInstance().GetScreenDPI() / 160.0f;
114 #endif
115 if (velocityMax > MINIMUM_SPEED_FOR_INERTIA * dpiScale)
117 if (velocityMax < MAXIMUM_SPEED_FOR_REDUCTION * dpiScale)
118 m_timeToZero = (m_timeToZero * velocityMax) / (MAXIMUM_SPEED_FOR_REDUCTION * dpiScale);
120 bool inertialRequested = false;
121 CGUIMessage message(GUI_MSG_GESTURE_NOTIFY, 0, 0, static_cast<int>(velocityX),
122 static_cast<int>(velocityY));
124 // ask if the control wants inertial scrolling
125 if (CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message))
127 int result = 0;
128 if (message.GetPointer())
130 int* p = static_cast<int*>(message.GetPointer());
131 message.SetPointer(nullptr);
132 result = *p;
133 delete p;
135 if (result == EVENT_RESULT_PAN_HORIZONTAL || result == EVENT_RESULT_PAN_VERTICAL)
137 inertialRequested = true;
141 if (inertialRequested)
143 m_iFlickVelocity.x = velocityX; // in pixels per sec
144 m_iFlickVelocity.y = velocityY; // in pixels per sec
145 m_iLastGesturePoint.x = action->GetAmount(2); // last gesture point x
146 m_iLastGesturePoint.y = action->GetAmount(3); // last gesture point y
148 // calc deacceleration for fullstop in TIME_TO_ZERO_SPEED secs
149 // v = a*t + v0 -> set v = 0 because we want to stop scrolling
150 // a = -v0 / t
151 m_inertialDeacceleration.x = -1 * m_iFlickVelocity.x / m_timeToZero;
152 m_inertialDeacceleration.y = -1 * m_iFlickVelocity.y / m_timeToZero;
154 m_inertialStartTime = CTimeUtils::GetFrameTime(); // start time of inertial scrolling
155 ret = true;
156 m_bScrolling = true; // activate the inertial scrolling animation
161 if (action->GetID() == ACTION_GESTURE_BEGIN || action->GetID() == ACTION_GESTURE_END ||
162 action->GetID() == ACTION_GESTURE_ABORT)
164 m_panPoints.clear();
167 return ret;
170 bool CInertialScrollingHandler::ProcessInertialScroll(float frameTime)
172 // do inertial scroll animation by sending gesture_pan
173 if (m_bScrolling)
175 float xMovement = 0.0;
176 float yMovement = 0.0;
178 // decrease based on negative acceleration
179 // calc the overall inertial scrolling time in secs
180 float absoluteInertialTime = (CTimeUtils::GetFrameTime() - m_inertialStartTime) / (float)1000;
182 // as long as we aren't over the overall inertial scroll time - do the deacceleration
183 if (absoluteInertialTime < m_timeToZero)
185 // v = s/t -> s = t * v
186 xMovement = frameTime * m_iFlickVelocity.x;
187 yMovement = frameTime * m_iFlickVelocity.y;
189 // save new gesture point
190 m_iLastGesturePoint.x += xMovement;
191 m_iLastGesturePoint.y += yMovement;
193 // fire the pan action
194 if (!g_application.OnAction(CAction(ACTION_GESTURE_PAN, 0, m_iLastGesturePoint.x,
195 m_iLastGesturePoint.y, xMovement, yMovement,
196 m_iFlickVelocity.x, m_iFlickVelocity.y)))
198 m_bAborting = true; // we are done
201 // calc new velocity based on deacceleration
202 // v = a*t + v0
203 m_iFlickVelocity.x = m_inertialDeacceleration.x * frameTime + m_iFlickVelocity.x;
204 m_iFlickVelocity.y = m_inertialDeacceleration.y * frameTime + m_iFlickVelocity.y;
206 // check if the signs are equal - which would mean we deaccelerated to long and reversed the
207 // direction
208 if ((m_inertialDeacceleration.x < 0) == (m_iFlickVelocity.x < 0))
210 m_iFlickVelocity.x = 0;
212 if ((m_inertialDeacceleration.y < 0) == (m_iFlickVelocity.y < 0))
214 m_iFlickVelocity.y = 0;
217 else // no movement -> done
219 m_bAborting = true; // we are done
223 // if we are done - or we where aborted
224 if (m_bAborting)
226 // fire gesture end action
227 g_application.OnAction(CAction(ACTION_GESTURE_END, 0, 0.0f, 0.0f, 0.0f, 0.0f));
228 m_bAborting = false;
229 m_bScrolling = false; // stop scrolling
230 m_iFlickVelocity.x = 0;
231 m_iFlickVelocity.y = 0;
234 return true;