• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 30237868584

27 Jul 2026 04:43AM UTC coverage: 15.388% (+0.09%) from 15.298%
30237868584

Pull #2146

github

web-flow
Merge 648972a7d into d36abedc4
Pull Request #2146: Per-platform relative mouse motion (pointer lock) instead of cursor warping

120 of 181 new or added lines in 7 files covered. (66.3%)

422 existing lines in 6 files now uncovered.

30591 of 198799 relevant lines covered (15.39%)

1072.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.0
/src/lib/score/graphics/InfiniteScroller.cpp
1
#include <score/graphics/InfiniteScroller.hpp>
2
#include <score/tools/Cursor.hpp>
3
#include <score/tools/PointerLock.hpp>
4

5
#include <QCoreApplication>
6
#include <QGraphicsItem>
7
#include <QGraphicsScene>
8
#include <QGraphicsSceneMouseEvent>
9
#include <QGraphicsView>
10
#include <QGuiApplication>
11
#include <QMouseEvent>
12
#include <QPointer>
13
#include <QScreen>
14
#include <QTimer>
15
#include <QWidget>
16
#include <QWindow>
17
#include <qpa/qwindowsysteminterface.h>
18

19
#include <cmath>
20

21
namespace score
22
{
23
namespace
24
{
25
bool relativeSession{};
26
bool discardNextPointerDelta{};
27
bool usedPointerPositions{};
28
quint64 relativeGeneration{};
29
QPointer<QWindow> relativeWindow{};
30
QMetaObject::Connection relativeWindowGone{};
43✔
31
QPointF virtualPos{};
32

33
// Long enough for the release the platform sends through its normal channel.
34
constexpr int relativeReleaseGraceMs = 50;
35

36
void endRelativeSession()
31✔
37
{
38
  QObject::disconnect(relativeWindowGone);
31✔
39
  relativeWindowGone = QMetaObject::Connection{};
31✔
40

41
  if(relativeSession)
31✔
42
    PointerLock::endRelative();
5✔
43
  relativeSession = false;
31✔
44
  relativeWindow.clear();
31✔
45
  ++relativeGeneration;
31✔
46
}
31✔
47

48
QWindow* windowOf(QGraphicsItem& item) noexcept
10✔
49
{
50
  auto* scene = item.scene();
10✔
51
  if(!scene)
10✔
52
    return nullptr;
7✔
53

54
  for(auto* view : scene->views())
3✔
NEW
55
    if(auto* w = view->window()->windowHandle())
×
NEW
56
      return w;
×
57
  return nullptr;
3✔
58
}
10✔
59

60
// Last resort for a backend that ends the drag without the widget ever seeing
61
// a release: an item left grabbed is as bad as a pointer left locked. Deferred,
62
// so that a release the widget does get keeps going through the usual path.
63
void scheduleRelativeTeardown(int delayMs, bool injectRelease)
3✔
64
{
65
  const quint64 generation = relativeGeneration;
3✔
66
  QTimer::singleShot(delayMs, qGuiApp, [generation, injectRelease] {
6✔
67
    if(!relativeSession || relativeGeneration != generation)
3✔
68
      return;
1✔
69

70
    auto* win = relativeWindow.data();
2✔
71
    endRelativeSession();
2✔
72

73
    if(injectRelease && win)
2✔
NEW
74
      QWindowSystemInterface::handleMouseEvent(
×
NEW
75
          win, win->mapFromGlobal(virtualPos), virtualPos, Qt::NoButton, Qt::LeftButton,
×
NEW
76
          QEvent::MouseButtonRelease, qGuiApp->keyboardModifiers());
×
77
  });
3✔
78
}
3✔
79

80
// QGuiApplication drops mouse moves that do not change the cursor position,
81
// which a locked pointer never does.
NEW
82
void onRelativeMotion(QPointF delta)
×
83
{
NEW
84
  auto* win = relativeWindow.data();
×
NEW
85
  if(!win)
×
86
  {
NEW
87
    scheduleRelativeTeardown(0, false);
×
NEW
88
    return;
×
89
  }
90

NEW
91
  const QRectF geom = win->geometry();
×
NEW
92
  virtualPos += delta;
×
NEW
93
  virtualPos.setX(qBound(geom.left(), virtualPos.x(), geom.right()));
×
NEW
94
  virtualPos.setY(qBound(geom.top(), virtualPos.y(), geom.bottom()));
×
95

NEW
96
  const QPointF local = win->mapFromGlobal(virtualPos);
×
NEW
97
  QMouseEvent ev{
×
98
      QEvent::MouseMove,
99
      local,
100
      local,
101
      virtualPos,
102
      Qt::NoButton,
NEW
103
      qGuiApp->mouseButtons(),
×
NEW
104
      qGuiApp->keyboardModifiers()};
×
NEW
105
  QCoreApplication::sendEvent(win, &ev);
×
NEW
106
}
×
107

108
void onRelativeRelease()
3✔
109
{
110
  scheduleRelativeTeardown(relativeReleaseGraceMs, true);
3✔
111
}
3✔
112

113
void watchForTeardown(QWindow* window)
5✔
114
{
115
  static const bool quitHandler = [] {
6✔
116
    QObject::connect(
1✔
117
        qGuiApp, &QCoreApplication::aboutToQuit, qGuiApp, [] { endRelativeSession(); });
1✔
118
    return true;
1✔
119
  }();
120
  Q_UNUSED(quitHandler);
121

122
  if(window)
5✔
123
  {
NEW
124
    relativeWindowGone = QObject::connect(
×
NEW
125
        window, &QObject::destroyed, qGuiApp, [] { endRelativeSession(); });
×
NEW
126
  }
×
127
}
5✔
128
}
129

130
QRectF InfiniteScroller::currentGeometry{};
131
double InfiniteScroller::origValue{};
132
double InfiniteScroller::currentSpeed{};
133
double InfiniteScroller::currentDelta{};
134

135
void InfiniteScroller::cancel()
29✔
136
{
137
  endRelativeSession();
29✔
138
  discardNextPointerDelta = false;
29✔
139
  usedPointerPositions = false;
29✔
140
}
29✔
141

142
void InfiniteScroller::start(QGraphicsItem& self, double orig)
10✔
143
{
144
  cancel();
10✔
145

146
  currentDelta = 0.;
10✔
147
  origValue = orig;
10✔
148

149
#if !defined(__EMSCRIPTEN__)
150
  self.setCursor(QCursor(Qt::BlankCursor));
10✔
151
#endif
152
  auto* screen = qGuiApp->screenAt(QCursor::pos());
10✔
153
  if(!screen)
10✔
NEW
154
    screen = qGuiApp->primaryScreen();
×
155
  currentGeometry = screen->availableGeometry();
10✔
156

157
  auto* window = windowOf(self);
10✔
158
  relativeWindow = window;
10✔
159
  virtualPos = QCursor::pos();
10✔
160
  relativeSession
161
      = PointerLock::beginRelative(window, &onRelativeMotion, &onRelativeRelease);
10✔
162
  if(relativeSession)
10✔
163
    watchForTeardown(window);
5✔
164
}
10✔
165

166
void InfiniteScroller::move_free(QGraphicsSceneMouseEvent* event)
24✔
167
{
168
  const double ratio = qGuiApp->keyboardModifiers() & Qt::CTRL ? 0.2 : 1.;
24✔
169

170
  // Decided per move rather than latched at start: the lock is granted
171
  // asynchronously and can be refused or dropped mid-drag, and the pointer
172
  // positions below still work when it is not held.
173
  if(relativeSession && PointerLock::active())
24✔
174
  {
175
    const double delta = ratio * PointerLock::takeDelta().y();
9✔
176
    currentSpeed = delta;
9✔
177
    currentDelta += delta;
9✔
178
    discardNextPointerDelta = true;
9✔
179
    return;
9✔
180
  }
181

182
  // The pointer did not move while it was locked, but the synthesized moves did:
183
  // the first delta measured after a relative session is the whole drag, backwards.
184
  if(discardNextPointerDelta)
15✔
185
  {
186
    discardNextPointerDelta = false;
2✔
187
    currentSpeed = 0.;
2✔
188
    return;
2✔
189
  }
190

191
  usedPointerPositions = true;
13✔
192

193
  const double delta = (event->screenPos().y() - event->lastScreenPos().y());
13✔
194
  if(std::abs(delta) < 500)
13✔
195
  {
196
    currentSpeed = ratio * delta;
13✔
197
    currentDelta += ratio * delta;
13✔
198
  }
13✔
199

200
  const double top = currentGeometry.top();
13✔
201
  const double bottom = currentGeometry.bottom();
13✔
202
  if(event->screenPos().y() <= top + 100)
13✔
203
  {
204
    score::setCursorPos(QPointF(event->screenPos().x(), bottom - 101));
1✔
205
  }
1✔
206
  else if(event->screenPos().y() >= bottom - 100)
12✔
207
  {
NEW
208
    score::setCursorPos(QPointF(event->screenPos().x(), top + 101));
×
NEW
209
  }
×
210
}
24✔
211

212
double InfiniteScroller::move(QGraphicsSceneMouseEvent* event)
15✔
213
{
214
  move_free(event);
15✔
215

216
  const double max = currentGeometry.height();
15✔
217
  double v = origValue - currentDelta / max;
15✔
218
  if(v <= 0.)
15✔
219
  {
NEW
220
    currentDelta = origValue * max;
×
NEW
221
    v = 0.;
×
NEW
222
  }
×
223
  else if(v >= 1.)
15✔
224
  {
NEW
225
    currentDelta = ((origValue - 1.) * max);
×
NEW
226
    v = 1.;
×
NEW
227
  }
×
228

229
  return v;
15✔
230
}
231

232
void InfiniteScroller::stop(QGraphicsItem& self, QGraphicsSceneMouseEvent* event)
9✔
233
{
234
  const bool restoreCursor = usedPointerPositions;
9✔
235
  cancel();
9✔
236

237
  if(restoreCursor)
9✔
238
    score::setCursorPos(event->buttonDownScreenPos(Qt::LeftButton));
6✔
239

240
  self.unsetCursor();
9✔
241
}
9✔
242
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc