Skip to content

Commit

Permalink
Merge branch 'MOUSE-WHEEL' - fix mouse wheel handling in modern Chrom…
Browse files Browse the repository at this point in the history
…e and Firefox

* MOUSE-WHEEL:
  ui/util.js: remove unused wheel2scrollbars
  ui/b/pianoroll.js: reinstante hzoom and vzoom adjustments via Ctrl
  ui/util.js: use Mouse.wheel_delta()
  ui/b/knob.js: use Mouse.wheel_delta()
  ui/b/pianoroll.js: properly scroll via deltaY from Mouse.wheel_delta()
  ui/mouse.js: wheel_delta(): provide .x, .y, .z in pixels
  ui/b/app.js: defer ZMove handling to mouse.js
  ui/mouse.js: include ZMove notification handling
  ui/index.html: start mouse.js early on
  ui/mouse.js: add event_movement() and wheel_delta() to fix event coords
	Cross browser normalization of movementX,movementY and deltaX,deltaY is a hot mess, see:
	https://bugs.chromium.org/p/chromium/issues/detail?id=1092358
	https://bugzilla.mozilla.org/show_bug.cgi?id=1392460
	w3c/uievents#181
	w3c/pointerlock#42
	mdn/content#11811
  ui/index.html: disable modulepreload, it causes module errors in Firefox-122

Signed-off-by: Tim Janik <[email protected]>
  • Loading branch information
tim-janik committed Feb 8, 2024
2 parents 8cfd329 + b9341a8 commit 070d91a
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 99 deletions.
41 changes: 6 additions & 35 deletions ui/b/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,15 @@
* Global application instance for Anklang.
* *zmovehooks*
* : An array of callbacks to be notified on pointer moves.
* *zmove (event)*
* : Trigger the callback list `zmovehooks`, passing `event` along. This is useful to get debounced
* *zmove()*
* : Trigger the callback list `zmovehooks`. This is useful to get debounced
* notifications for pointer movements, including 0-distance moves after significant UI changes.
*/

import VueComponents from '../all-components.js';
import ShellClass from '../b/shell.js';
import * as Util from '../util.js';

// == zmove() ==
class ZMove {
/** @type {Event} */
static last_event;
static
zmove (ev) {
if (ev && ev.screenX && ev.screenY &&
ev.timeStamp > (ZMove.last_event?.timeStamp || 0))
ZMove.last_event = ev;
ZMove.trigger();
}
static
trigger_hooks() {
if (ZMove.last_event)
for (const hook of ZMove.zmovehooks)
hook (ZMove.last_event);
}
static trigger = Util.debounce (ZMove.trigger_hooks);
static zmovehooks = [];
static zmoves_add (cb) {
ZMove.zmovehooks.push (cb);
return _ => {
Util.array_remove (ZMove.zmovehooks, cb);
};
}
}
document.addEventListener ("pointerdown", ZMove.zmove, { capture: true, passive: true });
document.addEventListener ("pointermove", ZMove.zmove, { capture: true, passive: true });
document.addEventListener ("pointerup", ZMove.zmove, { capture: true, passive: true });
import * as Mouse from '../mouse.js';

// == App ==
export class AppClass {
Expand Down Expand Up @@ -191,9 +162,9 @@ export class AppClass {
};
return this.shell.async_modal_dialog (dialog_setup);
}
zmoves_add = ZMove.zmoves_add;
zmove = ZMove.zmove;
zmove_last = () => ZMove.last_event;
zmoves_add = Mouse.zmove_add;
zmove = Mouse.zmove_trigger;
zmove_last = Mouse.zmove_last;
}

// == addvc ==
Expand Down
7 changes: 4 additions & 3 deletions ui/b/knob.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import { LitComponent, html, JsExtract, docs } from '../little.js';
import * as Util from "../util.js";
import * as Mouse from '../mouse.js';

// == STYLE ==
JsExtract.scss`
Expand Down Expand Up @@ -64,7 +65,7 @@ b-knob {

// == HTML ==
const HTML = (t,d) => html`
<div id="sprite" ?bidir=${d.bidir} @wheel="${t.wheel}"
<div id="sprite" ?bidir=${d.bidir} @wheel="${t.wheel_event}"
@pointerdown="${t.pointerdown}"
@dblclick="${Util.prevent_event}"
style="background-position: 0px calc(-270 * var(--pxsize))"></div>
Expand Down Expand Up @@ -172,9 +173,9 @@ class BKnob extends LitComponent {
this.reposition();
}
}
wheel (event)
wheel_event (event)
{
const d = Util.wheel_delta (event);
const d = Mouse.wheel_delta (event);
if (this[SPIN_DRAG]?.captureid === undefined && // not dragging
((!this.hscroll && d.x != 0) ||
(!this.vscroll && d.y != 0)))
Expand Down
25 changes: 17 additions & 8 deletions ui/b/pianoroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import { LitComponent, ref, html, JsExtract, docs } from '../little.js';
import * as PianoCtrl from "./piano-ctrl.js";
import * as Util from '../util.js';
import { clamp } from '../util.js';
import * as Mouse from '../mouse.js';
const floor = Math.floor, round = Math.round;

// == STYLE ==
Expand Down Expand Up @@ -77,7 +79,7 @@ const HTML = (t, d) => html`
<c-grid tabindex="-1" ${ref (h => t.cgrid = h)} data-f1="#piano-roll"
@pointerenter=${t.pointerenter} @pointerleave=${t.pointerleave} @focus=${t.focuschange} @blur=${t.focuschange}
@keydown=${e => t.piano_ctrl.keydown (e)}
@wheel=${t.wheel} >
@wheel=${t.wheel_event} >
<v-flex class="-toolbutton -col1 -row1" style="height: 1.7em; align-items: end; padding-right: 4px;" ${ref (h => t.menu_btn = h)}
@click=${e => t.pianotoolmenu.popup (e)} @mousedown=${e => t.pianotoolmenu.popup (e)} >
Expand Down Expand Up @@ -378,14 +380,21 @@ class BPianoRoll extends LitComponent {
paint_timeline.call (this);
paint_piano.call (this);
}
wheel (event)
wheel_event (event)
{
// TODO: use "scroll" for scrollbars, wheel delta is inappropriate
const delta = Util.wheel_delta (event);
if (Math.abs (delta.x) > Math.abs (delta.y))
this.hscrollbar.scrollBy ({ left: delta.x });
else
this.vscrollbar.scrollBy ({ top: delta.y });
const delta = Mouse.wheel_delta (event, true);
if (event.ctrlKey) {
if (delta.deltaX)
this.hzoom = clamp (this.hzoom * (delta.deltaX > 0 ? 1.1 : 0.9), 0.25, 25);
if (delta.deltaY)
this.vzoom = clamp (this.vzoom * (delta.deltaY > 0 ? 1.1 : 0.9), 0.5, 25);
this.request_update_();
} else {
if (delta.deltaX)
this.hscrollbar.scrollBy ({ left: delta.deltaX });
if (delta.deltaY)
this.vscrollbar.scrollBy ({ top: delta.deltaY });
}
Util.prevent_event (event);
}
}
Expand Down
26 changes: 14 additions & 12 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@
}
}
</script>
<link rel="modulepreload" href="./startup.js">
<link rel="modulepreload" href="./all-components.js">
<link rel="modulepreload" href="./browserified.js">
<link rel="modulepreload" href="./util.js">
<link rel="modulepreload" href="./vue.js">
<link rel="modulepreload" href="./aseapi.js">
<link rel="modulepreload" href="./little.js">
<link rel="modulepreload" href="./lit.js">
<link rel="modulepreload" href="./b/app.js">
<link rel="modulepreload" href="./b/shell.js">
<link rel="modulepreload" href="./b/piano-ctrl.js">
<link rel="modulepreload" href="./b/envue.js">
<!-- modulepreload triggers a race in Firefox-122 that causes ES6 module loading error
<link rel="modulepreload" href="./startup.js">
<link rel="modulepreload" href="./all-components.js">
<link rel="modulepreload" href="./browserified.js">
<link rel="modulepreload" href="./util.js">
<link rel="modulepreload" href="./vue.js">
<link rel="modulepreload" href="./aseapi.js">
<link rel="modulepreload" href="./little.js">
<link rel="modulepreload" href="./lit.js">
<link rel="modulepreload" href="./b/app.js">
<link rel="modulepreload" href="./b/shell.js">
<link rel="modulepreload" href="./b/piano-ctrl.js">
<link rel="modulepreload" href="./b/envue.js"> -->
<link rel="preload" as="style" href="global.css?csshash=@--CSSHASH--@">
<link rel="preload" as="style" href="assets/AnklangIcons.css">
<link rel="preload" as="style" href="material-icons.css">
Expand Down Expand Up @@ -61,6 +62,7 @@
<body>
<div id="b-app" class="b-app" style="display: flex; width: 100%; height: 100%; position: relative; z-index: 0;">
<script type="module" src="./startup.js"></script>
<script type="module" src="./mouse.js"></script>
<script type="module" > {
const b_app = document.querySelector ('#b-app');
b_app.innerHTML = '<div>ANKLANG: Importing Modules...</div>';
Expand Down
152 changes: 152 additions & 0 deletions ui/mouse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0

if (0) { // debug code
document.body.addEventListener ('wheel', e => console.log (wheel_delta (e, true)), { capture: true, passive: true });
document.body.addEventListener ('pointermove', e => console.log (chrome_dppx + ':', e.clientX - last_event.clientX, event_movement (e, true)), { capture: true, passive: true });
}

/// Flag indicating Chrome UserAgent.
// Indicates need for Chrome coordinate fixing.
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=1092358 https://github.com/w3c/pointerlock/issues/42
export const CHROME_UA = (() => {
const chrome_match = /\bChrome\/([0-9]+)\./.exec (navigator.userAgent);
if (chrome_match) {
const chrome_major = parseInt (chrome_match[1]);
return chrome_major >= 37; // introduction of movementX, movementY
}
return false;
}) ();

// Measure dots-per-physical-pixel as Chrome workaround
function body_move_event_4chrome (ev)
{
const prev = last_event;
const ret = body_move_event (ev);
if (prev && prev != last_event &&
prev.screenY != last_event.screenY) {
const cy = last_event.clientY - prev.clientY;
const sy = last_event.screenY - prev.screenY;
chrome_dppx = Math.min (Math.max (cy * devicePixelRatio / sy, 1), 4);
// Done, swap-in the real handler
document.body.removeEventListener ("pointermove", body_move_event_4chrome, { capture: true, passive: true });
document.body.addEventListener ("pointermove", body_move_event, { capture: true, passive: true });
}
return ret;
}
let chrome_dppx = 1; // dppx based on clientY/screenY

// Install global event listeners for mouse movements.
function body_move_event (event)
{
if (event.isTrusted &&
(!last_event || last_event.timeStamp <= event.timeStamp)) {
last_event = event;
zmove_trigger();
}
// FF-122: clientX scales with devicePixelRatio, movementX and screenX scale with devicePixelRatio
// CR-120: clientX scales with devicePixelRatio, movementX and screenX remain unscaled
}
let last_event = null;
document.body.addEventListener ("pointermove", CHROME_UA ? body_move_event_4chrome : body_move_event, { capture: true, passive: true });

/// Determine factor for Chrome to convert movementX to CSS px (based on event history)
export function chrome_movement_factor()
{
return chrome_dppx / devicePixelRatio;
}

/// Determine `movementX,movementY` in CSS pixels, independent of browser quirks.
export function event_movement (event, shift_swaps = false)
{
let mx = event.movementX, my = event.movementY;
if (CHROME_UA) {
const cf = chrome_movement_factor();
mx *= cf;
my *= cf;
}
let shiftSwapped = false;
if (shift_swaps && event.shiftKey) {
const tx = mx;
mx = my;
my = tx;
shiftSwapped = true;
}
return { movementX: mx, movementY: my, coalesced: event.getCoalescedEvents().length,
shiftSwapped, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, altKey: event.altKey };
// FF-122: movementX,movementY values in getCoalescedEvents() are utterly broken
}

/** Determine mouse wheel deltas in bare units, independent of zoom or DPI.
* This returns an object `{deltaX,deltaY}` with negative values pointing LEFT/UP and positive values RIGHT/DOWN respectively.
* For zoom step interpretation, the x/y pixel values should be reduced via `Math.sign()`.
* For scales the pixel values might feel more natural, because browsers sometimes increase the number of events with
* increasing wheel distance, in other cases values are accumulated so fewer events with larger deltas are sent instead.
* The members `{x,y}` approximate the wheel delta in pixels.
*/
export function wheel_delta (event, shift_swaps = false)
{
// The delta values must be read *first* in FF, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1392460#c33
let dx = event.deltaX, dy = event.deltaY, dz = event.deltaZ;
let dppx = CHROME_UA ? devicePixelRatio / Math.round (chrome_dppx) : 1;
// Also: https://github.com/facebook/Rapid/blob/724a7c92f73c295b87ca9b6a4568ce4e25074057/modules/pixi/PixiEvents.js#L359-L417
if (event.deltaMode == WheelEvent.DOM_DELTA_LINE)
dppx *= 8;
else if (event.deltaMode == WheelEvent.DOM_DELTA_PAGE)
dppx *= 24;
dx *= dppx;
dy *= dppx;
dz *= dppx;
let shiftSwapped = false;
if (shift_swaps && event.shiftKey && dx == 0 && dy) {
dx = dy;
dy = 0;
shiftSwapped = true;
}
let x = dx, y = dy, z = dz;
if (event.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
x *= 1.0 / 120;
y *= 1.0 / 120;
z *= 1.0 / 120;
}
return { deltaX: dx, deltaY: dy, deltaZ: dz, deltaMode: WheelEvent.DOM_DELTA_PIXEL, x, y, z,
shiftSwapped, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, altKey: event.altKey };
// FF-122: deltaY is mostly close to ±120 for mouse wheel on Linux regardless of zoom mode, and regardless of screen DPI
// CR-121: deltaY is ±120 for mouse wheel on Linux when devicePixelRatio=1, but is scaled along with zoom and screen DPI
// CR-120: deltaY is ±6 for touchpad on Linux
}

/// Peek at the last pointer coordination event.
export function zmove_last()
{
return last_event || null;
}

/// Add hook to be called once the mouse position or stacking state changes, returns deleter.
export function zmove_add (hook)
{
zmove_hooks.push (hook);
return () => {
const i = zmove_hooks.indexOf (hook);
i <= -1 || zmove_hooks.splice (i, 1);
};
}
const zmove_hooks = [];

let zmove_rafid;
function zmove_raf_handler()
{
zmove_rafid = undefined;
if (!last_event) return;
for (const hook of zmove_hooks)
hook (last_event);
}

/// Trigger zmove hooks, this is useful to get debounced notifications for pointer movements,
/// including 0-distance moves after significant UI changes.
export function zmove_trigger (ev = undefined)
{
if (!zmove_rafid)
zmove_rafid = requestAnimationFrame (zmove_raf_handler);
}
document.body.addEventListener ("pointerdown", zmove_trigger, { capture: true, passive: true });
document.body.addEventListener ("pointerup", zmove_trigger, { capture: true, passive: true });
42 changes: 1 addition & 41 deletions ui/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import * as Kbd from './kbd.js';
import * as Wrapper from './wrapper.js';
import * as Mouse from './mouse.js';

// == Compat fixes ==
class FallbackResizeObserver {
Expand Down Expand Up @@ -1199,47 +1200,6 @@ export function is_displayed (element) {
return false;
}

/** Retrieve normalized scroll wheel event delta in CSS pixels (across Browsers)
* This returns an object `{x,y}` with negative values pointing
* LEFT/UP and positive values RIGHT/DOWN respectively.
* For zoom step interpretation, the x/y pixel values should be
* reduced via `Math.sign()`.
* For scales the pixel values might feel more natural, because
* browsers sometimes increase the number of events with
* increasing wheel distance, in other cases values are accumulated
* so fewer events with larger deltas are sent instead.
*/
export function wheel_delta (ev)
{
// The delta values must be read *first* in FF, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1392460#c33
const deltaX = ev.deltaX, deltaY = ev.deltaY, deltaZ = ev.deltaZ;
// Also: https://github.com/facebook/Rapid/blob/724a7c92f73c295b87ca9b6a4568ce4e25074057/modules/pixi/PixiEvents.js#L359-L417
if (WheelEvent.DOM_DELTA_PAGE === ev.deltaMode)
return { x: deltaX * 24, y: deltaY * 24, z: deltaZ * 24 };
if (WheelEvent.DOM_DELTA_LINE === ev.deltaMode)
return { x: deltaX * 8, y: deltaY * 8, z: deltaZ * 8 };
if (WheelEvent.DOM_DELTA_PIXEL === ev.deltaMode)
return { x: deltaX / 120, y: deltaY / 120, z: deltaZ / 120 };
// legacy browsers
return { x: 0, y: (ev.detail || 0) / -3, z: 0 };
}

/** Use deltas from `event` to call scrollBy() on `refs[scrollbars...]`. */
export function wheel2scrollbars (event, refs, ...scrollbars)
{
const delta = wheel_delta (event);
for (const sb of scrollbars)
{
const scrollbar = refs[sb];
if (!scrollbar)
continue;
if (scrollbar.clientHeight > scrollbar.clientWidth) // vertical
scrollbar.scrollBy ({ top: delta.y });
else // horizontal
scrollbar.scrollBy ({ left: delta.x });
}
}

/** Setup Element shield for a modal containee.
* Capture focus movements inside `containee`, call `closer(event)` for
* pointer clicks on `shield` or when `ESCAPE` is pressed.
Expand Down

0 comments on commit 070d91a

Please sign in to comment.