Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions examples/mouse_drag_regression.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Browser mouse regression fixture. Inspect window.windyMouseEvents from CDP.
import windy, vmath

{.emit: """
#include <emscripten.h>
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()
42 changes: 35 additions & 7 deletions src/windy/platforms/emscripten/platform.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import
# include for EMSCRIPTEN_KEEPALIVE
{.emit: """
#include <emscripten.h>
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

Expand Down Expand Up @@ -734,34 +742,52 @@ 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

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

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.} =
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
32 changes: 32 additions & 0 deletions tests/browser_mouse_drag.js
Original file line number Diff line number Diff line change
@@ -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};
})()
Loading