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

ossia / score / 30182373935

26 Jul 2026 01:08AM UTC coverage: 15.307%. First build
30182373935

Pull #2146

github

web-flow
Merge f73f8c4da into 13afd939a
Pull Request #2146: Per-platform relative mouse motion (pointer lock) instead of cursor warping

1 of 128 new or added lines in 6 files covered. (0.78%)

30390 of 198538 relevant lines covered (15.31%)

983.96 hits per line

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

0.0
/src/lib/score/tools/PointerLock.cpp
1
#include <score/tools/PointerLock.hpp>
2

3
#if !defined(__APPLE__)
4

5
#include <QWindow>
6

7
#if defined(__EMSCRIPTEN__)
8
#include <emscripten/emscripten.h>
9
#include <emscripten/html5.h>
10

11
// Qt puts its windows in a shadow tree and matches events on composedPath()[0];
12
// anything else we lock makes QWasmWindow::processPointer drop every event for
13
// the duration of the lock, release included.
14
// clang-format off
15
EM_JS(void, score_pointerlock_install, (), {
16
  if (Module.scorePointerLock)
17
    return;
18
  const st = { target: null, x: 0, y: 0 };
19
  Module.scorePointerLock = st;
20
  // Only the press decides what gets locked: a move landing on another element
21
  // would otherwise retarget the lock and make Qt drop the release.
22
  document.addEventListener('pointerdown', (e) => {
23
    st.target = e.composedPath()[0] || e.target;
24
    st.x = e.clientX;
25
    st.y = e.clientY;
26
  }, true);
27
  document.addEventListener('pointerup', () => {
28
    if (document.pointerLockElement)
29
      document.exitPointerLock();
30
  }, true);
31
});
32

33
EM_JS(int, score_pointerlock_request, (), {
34
  const st = Module.scorePointerLock;
35
  let el = st ? st.target : null;
36
  if (!el || !el.isConnected) {
37
    el = document.elementFromPoint(st ? st.x : 0, st ? st.y : 0);
38
    while (el && el.shadowRoot) {
39
      const inner = el.shadowRoot.elementFromPoint(st.x, st.y);
40
      if (!inner || inner === el)
41
        break;
42
      el = inner;
43
    }
44
  }
45
  if (!el || !el.requestPointerLock)
46
    return 0;
47
  try {
48
    const p = el.requestPointerLock();
49
    // Refusal (no user activation, element gone) also raises pointerlockerror,
50
    // which is what actually drives the state; this only silences the rejection.
51
    if (p && p.catch)
52
      p.catch(() => {});
53
  } catch (e) {
54
    return 0;
55
  }
56
  return 1;
57
});
58
// clang-format on
59

60
namespace
61
{
62
struct PointerLockListeners
63
{
64
  PointerLockListeners() { score_pointerlock_install(); }
65
};
66

67
const PointerLockListeners g_listeners;
68
}
69

70
namespace score
71
{
72
namespace
73
{
74
enum class LockState
75
{
76
  Idle,
77
  Requested,
78
  Locked,
79
  Lost
80
};
81

82
LockState g_state{LockState::Idle};
83
PointerLock::MotionCallback g_callback{};
84
PointerLock::ReleaseCallback g_release{};
85
double g_dx{};
86
double g_dy{};
87

88
bool on_mousemove(int, const EmscriptenMouseEvent* e, void*)
89
{
90
  if(g_state != LockState::Locked)
91
    return false;
92

93
  g_dx += e->movementX;
94
  g_dy += e->movementY;
95
  if(g_callback)
96
    g_callback(QPointF(e->movementX, e->movementY));
97
  return false;
98
}
99

100
bool on_mouseup(int, const EmscriptenMouseEvent*, void*)
101
{
102
  if(g_state != LockState::Idle && g_release)
103
    g_release();
104
  return false;
105
}
106

107
bool on_lockchange(int, const EmscriptenPointerlockChangeEvent* e, void*)
108
{
109
  if(e->isActive)
110
    g_state = LockState::Locked;
111
  else if(g_state != LockState::Idle)
112
    g_state = LockState::Lost;
113
  return false;
114
}
115

116
bool on_lockerror(int, const void*, void*)
117
{
118
  if(g_state == LockState::Requested)
119
    g_state = LockState::Lost;
120
  return false;
121
}
122
}
123

124
bool PointerLock::beginRelative(
125
    QWindow*, MotionCallback onMotion, ReleaseCallback onRelease) noexcept
126
{
127
  if(g_state == LockState::Requested || g_state == LockState::Locked)
128
    return true;
129

130
  static const bool listeners = [] {
131
    emscripten_set_mousemove_callback(
132
        EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, true, &on_mousemove);
133
    emscripten_set_mouseup_callback(
134
        EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, true, &on_mouseup);
135
    emscripten_set_pointerlockchange_callback(
136
        EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, true, &on_lockchange);
137
    emscripten_set_pointerlockerror_callback(
138
        EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, true, &on_lockerror);
139
    return true;
140
  }();
141
  (void)listeners;
142

143
  if(!score_pointerlock_request())
144
    return false;
145

146
  g_callback = onMotion;
147
  g_release = onRelease;
148
  g_dx = 0.;
149
  g_dy = 0.;
150
  g_state = LockState::Requested;
151
  return true;
152
}
153

154
bool PointerLock::active() noexcept
155
{
156
  // Only once the browser has actually granted the lock: the request is
157
  // asynchronous and may be refused, and no motion is delivered until then.
158
  return g_state == LockState::Locked;
159
}
160

161
QPointF PointerLock::takeDelta() noexcept
162
{
163
  const QPointF d{g_dx, g_dy};
164
  g_dx = 0.;
165
  g_dy = 0.;
166
  return d;
167
}
168

169
void PointerLock::endRelative() noexcept
170
{
171
  if(g_state == LockState::Idle)
172
    return;
173

174
  g_state = LockState::Idle;
175
  g_callback = nullptr;
176
  g_release = nullptr;
177
  g_dx = 0.;
178
  g_dy = 0.;
179
  emscripten_exit_pointerlock();
180
}
181
}
182

183
#elif defined(_WIN32)
184
#include <QAbstractNativeEventFilter>
185
#include <QByteArray>
186
#include <QGuiApplication>
187

188
#include <windows.h>
189

190
#include <vector>
191

192
namespace score
193
{
194
namespace
195
{
196
double g_dx{};
197
double g_dy{};
198
PointerLock::MotionCallback g_callback{};
199
PointerLock::ReleaseCallback g_release{};
200
HWND g_window{};
201

202
struct RawMouseFilter final : public QAbstractNativeEventFilter
203
{
204
  bool nativeEventFilter(const QByteArray&, void* message, qintptr*) override
205
  {
206
    auto* msg = static_cast<MSG*>(message);
207
    if(msg->message != WM_INPUT || !g_window)
208
      return false;
209

210
    UINT size = 0;
211
    if(GetRawInputData(
212
           (HRAWINPUT)msg->lParam, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER))
213
       != 0)
214
      return false;
215

216
    m_buffer.resize(size);
217
    if(GetRawInputData(
218
           (HRAWINPUT)msg->lParam, RID_INPUT, m_buffer.data(), &size,
219
           sizeof(RAWINPUTHEADER))
220
       != size)
221
      return false;
222

223
    const auto& raw = *reinterpret_cast<const RAWINPUT*>(m_buffer.data());
224
    if(raw.header.dwType != RIM_TYPEMOUSE)
225
      return false;
226
    if(raw.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE)
227
      return false;
228

229
    if(raw.data.mouse.lLastX != 0 || raw.data.mouse.lLastY != 0)
230
    {
231
      const QPointF delta(raw.data.mouse.lLastX, raw.data.mouse.lLastY);
232
      g_dx += delta.x();
233
      g_dy += delta.y();
234
      if(g_callback)
235
        g_callback(delta);
236
    }
237

238
    if((raw.data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_UP) && g_release)
239
      g_release();
240
    return false;
241
  }
242

243
private:
244
  std::vector<char> m_buffer;
245
};
246

247
RawMouseFilter g_filter;
248
}
249

250
bool PointerLock::beginRelative(
251
    QWindow* window, MotionCallback onMotion, ReleaseCallback onRelease) noexcept
252
{
253
  if(g_window)
254
    return true;
255
  if(!window)
256
    return false;
257

258
  auto hwnd = (HWND)window->winId();
259
  if(!hwnd)
260
    return false;
261

262
  RAWINPUTDEVICE rid{};
263
  rid.usUsagePage = 0x01;
264
  rid.usUsage = 0x02;
265
  rid.dwFlags = RIDEV_INPUTSINK;
266
  rid.hwndTarget = hwnd;
267
  if(!RegisterRawInputDevices(&rid, 1, sizeof(rid)))
268
    return false;
269

270
  g_dx = 0.;
271
  g_dy = 0.;
272
  g_callback = onMotion;
273
  g_release = onRelease;
274
  g_window = hwnd;
275
  qGuiApp->installNativeEventFilter(&g_filter);
276

277
  POINT p{};
278
  if(GetCursorPos(&p))
279
  {
280
    const RECT r{p.x, p.y, p.x + 1, p.y + 1};
281
    ClipCursor(&r);
282
  }
283
  return true;
284
}
285

286
bool PointerLock::active() noexcept
287
{
288
  return g_window != nullptr;
289
}
290

291
QPointF PointerLock::takeDelta() noexcept
292
{
293
  const QPointF d{g_dx, g_dy};
294
  g_dx = 0.;
295
  g_dy = 0.;
296
  return d;
297
}
298

299
void PointerLock::endRelative() noexcept
300
{
301
  if(!g_window)
302
    return;
303

304
  ClipCursor(nullptr);
305
  qGuiApp->removeNativeEventFilter(&g_filter);
306

307
  RAWINPUTDEVICE rid{};
308
  rid.usUsagePage = 0x01;
309
  rid.usUsage = 0x02;
310
  rid.dwFlags = RIDEV_REMOVE;
311
  rid.hwndTarget = nullptr;
312
  RegisterRawInputDevices(&rid, 1, sizeof(rid));
313

314
  g_window = nullptr;
315
  g_callback = nullptr;
316
  g_release = nullptr;
317
  g_dx = 0.;
318
  g_dy = 0.;
319
}
320
}
321

322
#elif defined(SCORE_HAS_WAYLAND_POINTER_LOCK)
323
#include <QGuiApplication>
324
#include <qpa/qplatformnativeinterface.h>
325

326
#include <cstring>
327

328
#include <pointer-constraints-unstable-v1-client-protocol.h>
329
#include <relative-pointer-unstable-v1-client-protocol.h>
330
#include <wayland-client.h>
331

332
namespace score
333
{
334
namespace
335
{
336
struct WaylandGlobals
337
{
338
  wl_display* display{};
339
  wl_pointer* pointer{};
340
  zwp_relative_pointer_manager_v1* relative_manager{};
341
  zwp_pointer_constraints_v1* constraints{};
342
  bool tried{};
343
};
344

345
WaylandGlobals g_wl;
346
zwp_relative_pointer_v1* g_relative{};
347
zwp_locked_pointer_v1* g_locked{};
348
PointerLock::MotionCallback g_callback{};
349
PointerLock::ReleaseCallback g_release{};
350
double g_dx{};
351
double g_dy{};
352

353
void registry_global(
354
    void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t)
355
{
356
  auto& g = *static_cast<WaylandGlobals*>(data);
357
  if(std::strcmp(interface, zwp_relative_pointer_manager_v1_interface.name) == 0)
358
  {
359
    g.relative_manager = static_cast<zwp_relative_pointer_manager_v1*>(
360
        wl_registry_bind(registry, name, &zwp_relative_pointer_manager_v1_interface, 1));
361
  }
362
  else if(std::strcmp(interface, zwp_pointer_constraints_v1_interface.name) == 0)
363
  {
364
    g.constraints = static_cast<zwp_pointer_constraints_v1*>(
365
        wl_registry_bind(registry, name, &zwp_pointer_constraints_v1_interface, 1));
366
  }
367
}
368

369
void registry_global_remove(void*, wl_registry*, uint32_t) { }
370

371
const wl_registry_listener registry_listener{registry_global, registry_global_remove};
372

373
void relative_motion(
374
    void*, zwp_relative_pointer_v1*, uint32_t, uint32_t, wl_fixed_t dx, wl_fixed_t dy,
375
    wl_fixed_t, wl_fixed_t)
376
{
377
  const QPointF delta{wl_fixed_to_double(dx), wl_fixed_to_double(dy)};
378
  g_dx += delta.x();
379
  g_dy += delta.y();
380
  if(g_callback)
381
    g_callback(delta);
382
}
383

384
const zwp_relative_pointer_v1_listener relative_listener{relative_motion};
385

386
WaylandGlobals* globals()
387
{
388
  if(!g_wl.tried)
389
  {
390
    g_wl.tried = true;
391

392
    if(!qGuiApp->platformName().startsWith(QStringLiteral("wayland")))
393
      return nullptr;
394

395
    auto* ni = qGuiApp->platformNativeInterface();
396
    if(!ni)
397
      return nullptr;
398

399
    g_wl.display
400
        = static_cast<wl_display*>(ni->nativeResourceForIntegration("wl_display"));
401
    if(!g_wl.display)
402
      return nullptr;
403

404
    g_wl.pointer
405
        = static_cast<wl_pointer*>(ni->nativeResourceForIntegration("wl_pointer"));
406
    if(!g_wl.pointer)
407
    {
408
      if(auto* seat = static_cast<wl_seat*>(ni->nativeResourceForIntegration("wl_seat")))
409
        g_wl.pointer = wl_seat_get_pointer(seat);
410
    }
411
    if(!g_wl.pointer)
412
      return nullptr;
413

414
    auto* queue = wl_display_create_queue(g_wl.display);
415
    auto* registry = wl_display_get_registry(g_wl.display);
416
    wl_proxy_set_queue((wl_proxy*)registry, queue);
417
    wl_registry_add_listener(registry, &registry_listener, &g_wl);
418
    wl_display_roundtrip_queue(g_wl.display, queue);
419
    wl_registry_destroy(registry);
420
    wl_event_queue_destroy(queue);
421

422
    if(g_wl.relative_manager)
423
      wl_proxy_set_queue((wl_proxy*)g_wl.relative_manager, nullptr);
424
    if(g_wl.constraints)
425
      wl_proxy_set_queue((wl_proxy*)g_wl.constraints, nullptr);
426
  }
427

428
  return g_wl.constraints && g_wl.relative_manager ? &g_wl : nullptr;
429
}
430
}
431

432
bool PointerLock::beginRelative(
433
    QWindow* window, MotionCallback onMotion, ReleaseCallback onRelease) noexcept
434
{
435
  if(g_relative)
436
    return true;
437
  if(!window)
438
    return false;
439

440
  auto* g = globals();
441
  if(!g)
442
    return false;
443

444
  auto* surface = static_cast<wl_surface*>(
445
      qGuiApp->platformNativeInterface()->nativeResourceForWindow("surface", window));
446
  if(!surface)
447
    return false;
448

449
  g_relative = zwp_relative_pointer_manager_v1_get_relative_pointer(
450
      g->relative_manager, g->pointer);
451
  if(!g_relative)
452
    return false;
453

454
  zwp_relative_pointer_v1_add_listener(g_relative, &relative_listener, nullptr);
455
  g_locked = zwp_pointer_constraints_v1_lock_pointer(
456
      g->constraints, surface, g->pointer, nullptr,
457
      ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT);
458

459
  g_callback = onMotion;
460
  g_release = onRelease;
461
  g_dx = 0.;
462
  g_dy = 0.;
463
  wl_display_flush(g->display);
464
  return true;
465
}
466

467
bool PointerLock::active() noexcept
468
{
469
  return g_relative != nullptr;
470
}
471

472
QPointF PointerLock::takeDelta() noexcept
473
{
474
  const QPointF d{g_dx, g_dy};
475
  g_dx = 0.;
476
  g_dy = 0.;
477
  return d;
478
}
479

480
void PointerLock::endRelative() noexcept
481
{
482
  if(g_locked)
483
  {
484
    zwp_locked_pointer_v1_destroy(g_locked);
485
    g_locked = nullptr;
486
  }
487
  if(g_relative)
488
  {
489
    zwp_relative_pointer_v1_destroy(g_relative);
490
    g_relative = nullptr;
491
  }
492
  if(g_wl.display)
493
    wl_display_flush(g_wl.display);
494

495
  g_callback = nullptr;
496
  g_release = nullptr;
497
  g_dx = 0.;
498
  g_dy = 0.;
499
}
500
}
501

502
#else
503

504
namespace score
505
{
NEW
506
bool PointerLock::beginRelative(QWindow*, MotionCallback, ReleaseCallback) noexcept
×
507
{
NEW
508
  return false;
×
509
}
510

NEW
511
bool PointerLock::active() noexcept
×
512
{
NEW
513
  return false;
×
514
}
515

NEW
516
QPointF PointerLock::takeDelta() noexcept
×
517
{
NEW
518
  return {};
×
519
}
520

NEW
521
void PointerLock::endRelative() noexcept { }
×
522
}
523

524
#endif
525
#endif
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