diff --git a/examples/mouse_drag_regression.nim b/examples/mouse_drag_regression.nim new file mode 100644 index 0000000..462e7a1 --- /dev/null +++ b/examples/mouse_drag_regression.nim @@ -0,0 +1,24 @@ +## Browser mouse regression fixture. Inspect window.windyMouseEvents from CDP. +import windy, vmath + +{.emit: """ +#include +EM_JS(void, recordMouse, (int kind, int x, int y, int dx, int dy, int down), { + (window.windyMouseEvents ||= []).push({kind, x, y, dx, dy, down}); +}); +""".} +proc recordMouse(kind, x, y, dx, dy, down: cint) {.importc, nodecl.} +let window = newWindow("Windy mouse regression", ivec2(800, 600)) +proc snapshot(kind: cint) = + let position = window.mousePos.vec2 / window.contentScale + let delta = window.mouseDelta.vec2 / window.contentScale + recordMouse(kind, position.x.cint, position.y.cint, + delta.x.cint, delta.y.cint, cint(window.buttonDown[MouseLeft])) +window.onButtonPress = proc(button: Button) = + if button == MouseLeft: snapshot(1) +window.onButtonRelease = proc(button: Button) = + if button == MouseLeft: snapshot(2) +window.onMouseMove = proc() = snapshot(3) +window.onFrame = proc() = discard +while not window.closeRequested: + pollEvents() diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index abbd620..eca7460 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -5,7 +5,15 @@ import # include for EMSCRIPTEN_KEEPALIVE {.emit: """ #include +EM_JS(double, windy_canvas_left, (), { + return Module.canvas.getBoundingClientRect().left; +}); +EM_JS(double, windy_canvas_top, (), { + return Module.canvas.getBoundingClientRect().top; +}); """.} +proc windy_canvas_left(): cdouble {.importc, nodecl.} +proc windy_canvas_top(): cdouble {.importc, nodecl.} import emdefs @@ -734,10 +742,24 @@ proc mouseButtonToButton(button: cushort): Button = else: MouseButton4 # Event handlers +proc updateMousePosition(window: Window, mouseEvent: ptr EmscriptenMouseEvent) = + ## Use canvas-relative coordinates for both canvas and document events. + let position = ivec2( + int32(mouseEvent.clientX.float64 - windy_canvas_left()), + int32(mouseEvent.clientY.float64 - windy_canvas_top()) + ) + window.state.mousePrevPos = window.state.mousePos + if window.state.hasPrevMouse: + window.state.perFrame.mouseDelta += position - window.state.mousePos + window.state.mousePos = position + window.state.hasPrevMouse = true + proc onMouseDown(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData: pointer): EM_BOOL {.cdecl.} = let window = cast[Window](userData) # Ensure canvas has focus when clicked make_canvas_focusable() + window.updateMousePosition(mouseEvent) + window.state.mouseInside = true let button = mouseButtonToButton(mouseEvent.button) window.handleButtonPress(button) return 1 @@ -745,16 +767,18 @@ proc onMouseDown(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData proc onMouseUp(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData: pointer): EM_BOOL {.cdecl.} = let window = cast[Window](userData) let button = mouseButtonToButton(mouseEvent.button) + if button notin window.state.buttonDown: + return 0 + window.updateMousePosition(mouseEvent) window.handleButtonRelease(button) return 1 proc onMouseMove(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData: pointer): EM_BOOL {.cdecl.} = let window = cast[Window](userData) - window.state.mousePrevPos = window.state.mousePos - # Use clientX/clientY as they are more reliably populated - window.state.mousePos = ivec2(mouseEvent.clientX.int32, mouseEvent.clientY.int32) - window.state.mouseInside = true - window.state.perFrame.mouseDelta += window.state.mousePos - window.state.mousePrevPos + if not window.state.mouseInside and + window.state.buttonDown * {MouseLeft, MouseMiddle, MouseRight, MouseButton4} == {}: + return 0 + window.updateMousePosition(mouseEvent) if window.onMouseMove != nil: window.onMouseMove() return 1 @@ -762,6 +786,8 @@ proc onMouseMove(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData proc onMouseEnter(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData: pointer): EM_BOOL {.cdecl.} = let window = cast[Window](userData) window.state.mouseInside = true + window.state.hasPrevMouse = false + window.updateMousePosition(mouseEvent) return 1 proc onMouseLeave(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData: pointer): EM_BOOL {.cdecl.} = @@ -848,6 +874,8 @@ proc onBlur(eventType: cint, focusEvent: ptr EmscriptenFocusEvent, userData: poi ## Releases held input before notifying the application of focus loss. let window = cast[Window](userData) clearButtonsTemplate() + window.state.hasPrevMouse = false + window.state.perFrame.mouseDelta = ivec2(0) if frameDispatchActive: pendingFocusChange = true return 1 @@ -883,8 +911,8 @@ proc drainDeferredWindowEvents() = proc setupEventHandlers(window: Window) = # Mouse events discard emscripten_set_mousedown_callback_on_thread(window.canvas, cast[pointer](window), 1, onMouseDown, EM_CALLBACK_THREAD_CONTEXT) - discard emscripten_set_mouseup_callback_on_thread(window.canvas, cast[pointer](window), 1, onMouseUp, EM_CALLBACK_THREAD_CONTEXT) - discard emscripten_set_mousemove_callback_on_thread(window.canvas, cast[pointer](window), 1, onMouseMove, EM_CALLBACK_THREAD_CONTEXT) + discard emscripten_set_mouseup_callback_on_thread(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, cast[pointer](window), 1, onMouseUp, EM_CALLBACK_THREAD_CONTEXT) + discard emscripten_set_mousemove_callback_on_thread(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, cast[pointer](window), 1, onMouseMove, EM_CALLBACK_THREAD_CONTEXT) discard emscripten_set_mouseenter_callback_on_thread(window.canvas, cast[pointer](window), 1, onMouseEnter, EM_CALLBACK_THREAD_CONTEXT) discard emscripten_set_mouseleave_callback_on_thread(window.canvas, cast[pointer](window), 1, onMouseLeave, EM_CALLBACK_THREAD_CONTEXT) diff --git a/tests/browser_mouse_drag.js b/tests/browser_mouse_drag.js new file mode 100644 index 0000000..d1db580 --- /dev/null +++ b/tests/browser_mouse_drag.js @@ -0,0 +1,32 @@ +// Run in the mouse_drag_regression.html fixture after its WASM initializes. +(() => { + const c = document.querySelector('canvas'); + window.windyMouseEvents = []; + function mouse(target, type, x, y, buttons = 0) { + target.dispatchEvent(new MouseEvent(type, { + clientX: x, clientY: y, button: 0, buttons, bubbles: true + })); + } + mouse(c, 'mouseenter', 100, 100); + mouse(c, 'mousemove', 100, 100); + // No preceding move: activation clicks must still use their own coordinates. + mouse(c, 'mousedown', 220, 180, 1); + mouse(c, 'mouseleave', 810, 180, 1); + mouse(document, 'mousemove', 820, 190, 1); + mouse(document, 'mouseup', 830, 200); + mouse(c, 'mouseenter', 300, 200); + mouse(c, 'mousemove', 300, 200); + const events = window.windyMouseEvents; + const press = events.find(e => e.kind === 1); + const release = events.find(e => e.kind === 2); + const results = { + pressUsesCurrentPosition: press?.x === 220 && press?.y === 180, + dragOutsideDelivered: events.some(e => e.kind === 3 && e.x === 820), + releaseOutsideDelivered: release?.x === 830 && release?.y === 200, + buttonUpAfterReentry: events.at(-1)?.down === 0 + }; + if (!Object.values(results).every(Boolean)) { + throw Error(JSON.stringify({results, events})); + } + return {results, events}; +})()