|
4 | 4 | import { evaluateMathExpression } from "@graphite/../wasm/pkg/graphite_wasm.js"; |
5 | 5 | import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input"; |
6 | 6 | import { type NumberInputMode, type NumberInputIncrementBehavior } from "@graphite/messages"; |
| 7 | + import { browserVersion, isDesktop } from "@graphite/utility-functions/platform"; |
7 | 8 |
|
8 | 9 | import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; |
9 | 10 | import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte"; |
|
315 | 316 | // Remove the text entry cursor from any other selected text field |
316 | 317 | if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); |
317 | 318 |
|
318 | | - // Don't drag the text value from is input element |
| 319 | + // Don't drag the text value from its input element |
319 | 320 | e.preventDefault(); |
320 | 321 |
|
321 | | - // Now we need to wait and see if the user follows this up with a mousemove or mouseup. |
| 322 | + // Now we need to wait and see if the user follows this up with a mousemove or mouseup... |
322 | 323 |
|
323 | 324 | // For some reason, both events can get fired before their event listeners are removed, so we need to guard against both running. |
324 | 325 | let alreadyActedGuard = false; |
|
327 | 328 | const onMove = () => { |
328 | 329 | if (alreadyActedGuard) return; |
329 | 330 | alreadyActedGuard = true; |
| 331 | +
|
330 | 332 | isDragging = true; |
331 | 333 | beginDrag(e); |
| 334 | +
|
332 | 335 | removeEventListener("pointermove", onMove); |
| 336 | + removeEventListener("pointerup", onUp); |
333 | 337 | }; |
334 | 338 | // If it's a mouseup, we'll begin editing the text field. |
335 | 339 | const onUp = () => { |
336 | 340 | if (alreadyActedGuard) return; |
337 | 341 | alreadyActedGuard = true; |
| 342 | +
|
338 | 343 | isDragging = false; |
339 | 344 | self?.focus(); |
| 345 | +
|
| 346 | + removeEventListener("pointermove", onMove); |
340 | 347 | removeEventListener("pointerup", onUp); |
341 | 348 | }; |
342 | 349 | addEventListener("pointermove", onMove); |
|
348 | 355 | const target = e.target || undefined; |
349 | 356 | if (!(target instanceof HTMLElement)) return; |
350 | 357 |
|
| 358 | + // Default to using pointer lock except on unsupported platforms (Safari and the native desktop app). |
| 359 | + // Even though Safari supposedly supports the PointerLock API, it implements an old (2016) version of the spec that requires an "engagement |
| 360 | + // gesture" (<https://www.w3.org/TR/2016/REC-pointerlock-20161027/#glossary>) to initiate pointer lock, which the newer spec doesn't require. |
| 361 | + // Because "mousemove" (and similarly, the "pointermove" event we use) is defined as not being a user-initiated "engagement gesture" event, |
| 362 | + // Safari never lets us to enter pointer lock while the mouse button is held down and we are awaiting movement to begin dragging the slider. |
| 363 | + const isSafari = browserVersion().toLowerCase().includes("safari"); |
| 364 | + const usePointerLock = !isSafari && !isDesktop(); |
| 365 | +
|
| 366 | + // On Safari, we use a workaround involving an alternative strategy where we hide the cursor while it's within the web page |
| 367 | + // (but we can't hide it when it ventures outside the page), taking advantage of a separate (helpful) Safari bug where it |
| 368 | + // keeps reporting deltas to the pointer position even as it pushes up against edges of the screen. Like the PointerLock API, |
| 369 | + // this allows infinite movement in each direction, but the downside is that the cursor remains visible outside the page and |
| 370 | + // it ends up in that location when the drag ends. It isn't possible to take advantage of the PointerCapture API (yes, that's |
| 371 | + // a different API than PointerLock) which is supposed to allow the CSS cursor style (such as `cursor: none`) to persist even |
| 372 | + // while dragging it outside the browser window, because Safari has another bug where the cursor icon is unaffected by that API. |
| 373 | + if (isSafari) document.body.classList.add("cursor-hidden"); |
| 374 | +
|
351 | 375 | // Enter dragging state |
352 | | - target.requestPointerLock(); |
| 376 | + if (usePointerLock) target.requestPointerLock(); |
353 | 377 | initialValueBeforeDragging = value; |
354 | 378 | cumulativeDragDelta = 0; |
355 | 379 |
|
|
359 | 383 | // We ignore the first event invocation's `e.movementX` value because it's unreliable. |
360 | 384 | // In both Chrome and Firefox (tested on Windows 10), the first `e.movementX` value is occasionally a very large number |
361 | 385 | // (around positive 1000, even if movement was in the negative direction). This seems to happen more often if the movement is rapid. |
| 386 | + // TODO: On rarer occasions, it isn't sufficient to ignore just the first event, so this solution is imperfect. |
| 387 | + // TODO: Using a counter to ignore more frames helps progressively decrease—but not eliminate—the issue, but it makes drag initiation feel delayed so we don't do that. |
| 388 | + // TODO: A better solution will need to discard outlier movement values across multiple frames by basically implementing a time-series data analysis filtering algorithm. |
362 | 389 | let ignoredFirstMovement = false; |
363 | 390 |
|
364 | 391 | const pointerUp = () => { |
|
367 | 394 | initialValueBeforeDragging = value; |
368 | 395 | cumulativeDragDelta = 0; |
369 | 396 |
|
370 | | - document.exitPointerLock(); |
371 | | -
|
372 | | - // Fallback for Safari in case pointerlockchange never fires |
373 | | - setTimeout(() => { |
374 | | - if (!document.pointerLockElement) pointerLockChange(); |
375 | | - }, 0); |
| 397 | + if (usePointerLock) document.exitPointerLock(); |
| 398 | + else pointerLockChange(); |
376 | 399 | }; |
377 | 400 | const pointerMove = (e: PointerEvent) => { |
| 401 | + // TODO: Display a fake cursor over the top of the page which wraps around the edges of the editor. |
| 402 | +
|
378 | 403 | // Abort the drag if right click is down. This works here because a "pointermove" event is fired when right clicking even if the cursor didn't move. |
379 | 404 | if (e.buttons & BUTTONS_RIGHT) { |
380 | | - document.exitPointerLock(); |
| 405 | + if (usePointerLock) document.exitPointerLock(); |
| 406 | + else pointerLockChange(); |
381 | 407 | return; |
382 | 408 | } |
383 | 409 |
|
384 | | - // If no buttons are down, we are stuck in the drag state after having released the mouse, so we should exit. |
385 | | - // For some reason on firefox in wayland the button is -1 and the buttons is 0. |
| 410 | + // If no buttons are down, that means we are stuck in the drag state after having released the mouse, so we should exit. |
| 411 | + // For some reason on Firefox in Wayland, `e.buttons` can be 0 while `e.button` is -1, but we don't want to exit in that state. |
386 | 412 | if (e.buttons === 0 && e.button !== -1) { |
387 | | - document.exitPointerLock(); |
| 413 | + if (usePointerLock) document.exitPointerLock(); |
| 414 | + else pointerLockChange(); |
388 | 415 | return; |
389 | 416 | } |
390 | 417 |
|
|
408 | 435 | }; |
409 | 436 | const pointerLockChange = () => { |
410 | 437 | // Do nothing if we just entered, rather than exited, pointer lock. |
411 | | - if (document.pointerLockElement) return; |
| 438 | + if (usePointerLock && document.pointerLockElement) return; |
| 439 | +
|
| 440 | + // Un-hide the cursor if we're using the Safari workaround. |
| 441 | + if (isSafari) document.body.classList.remove("cursor-hidden"); |
412 | 442 |
|
413 | 443 | // Reset the value to the initial value if the drag was aborted, or to the current value if it was just confirmed by changing the initial value to the current value. |
414 | 444 | updateValue(initialValueBeforeDragging); |
|
418 | 448 | // Clean up the event listeners. |
419 | 449 | removeEventListener("pointerup", pointerUp); |
420 | 450 | removeEventListener("pointermove", pointerMove); |
421 | | - document.removeEventListener("pointerlockchange", pointerLockChange); |
| 451 | + if (usePointerLock) document.removeEventListener("pointerlockchange", pointerLockChange); |
422 | 452 | }; |
423 | 453 |
|
424 | 454 | addEventListener("pointerup", pointerUp); |
425 | 455 | addEventListener("pointermove", pointerMove); |
426 | | - document.addEventListener("pointerlockchange", pointerLockChange); |
| 456 | + if (usePointerLock) document.addEventListener("pointerlockchange", pointerLockChange); |
427 | 457 | } |
428 | 458 |
|
429 | 459 | // =============================== |
|
0 commit comments