From 05ea0a9f02f19e25daaf76dcecdfb344443e42c6 Mon Sep 17 00:00:00 2001 From: monofuel Date: Tue, 17 Feb 2026 16:11:31 -0500 Subject: [PATCH 1/6] fix: normalize Emscripten wheel events by deltaMode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raw WheelEvent.deltaX/Y values are not comparable across OS/browser combinations. Linux browsers (Chrome/Firefox) typically report ~120px per notch in DOM_DELTA_PIXEL mode, while macOS trackpads send small continuous values, making scroll feel ~10x too fast on Linux web builds. Normalize each deltaMode to target ~10 units per notch, matching the native Linux X11 backend (fixed ±10) and macOS backend (scrollingDeltaX/Y * 0.1 * 10 getter). Also removes the leftover debug echo from the onWheel handler. Co-Authored-By: Claude Sonnet 4.6 --- src/windy/platforms/emscripten/platform.nim | 27 ++++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index 51d877e..618c9f9 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -555,10 +555,29 @@ proc onMouseMove(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData proc onWheel(eventType: cint, wheelEvent: ptr EmscriptenWheelEvent, userData: pointer): EM_BOOL {.cdecl.} = let window = cast[Window](userData) - # Normalize web wheel events to match other platforms. - let normalizedDeltaX = wheelEvent.deltaX.float32 - let normalizedDeltaY = wheelEvent.deltaY.float32 - window.state.perFrame.scrollDelta += vec2(normalizedDeltaX, normalizedDeltaY) + + var x = wheelEvent.deltaX.float32 + var y = wheelEvent.deltaY.float32 + + # Normalize to match native backends (~10 units per scroll notch). + # DOM_DELTA_PIXEL (0): browsers report ~100-120px per notch on Linux. + # DOM_DELTA_LINE (1): browsers report ~3 lines per notch. + # DOM_DELTA_PAGE (2): one full page. + case wheelEvent.deltaMode + of 0: # DOM_DELTA_PIXEL + x *= 0.1'f32 + y *= 0.1'f32 + of 1: # DOM_DELTA_LINE + x *= 10.0'f32 / 3.0'f32 + y *= 10.0'f32 / 3.0'f32 + of 2: # DOM_DELTA_PAGE + let pageHeight = max(1'f32, window.size.y.float32 * 0.9'f32) + x *= pageHeight + y *= pageHeight + else: + discard + + window.state.perFrame.scrollDelta += vec2(x, y) if window.onScroll != nil: window.onScroll() return 1 From 9bcae38cb99faa7b81663782fd2aa610333b99dc Mon Sep 17 00:00:00 2001 From: treeform Date: Tue, 17 Feb 2026 15:24:04 -0800 Subject: [PATCH 2/6] Add debugging. --- examples/wheel_rect_boxy.nim | 66 +++++++++++++++++++++ src/windy/platforms/emscripten/platform.nim | 7 ++- 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 examples/wheel_rect_boxy.nim diff --git a/examples/wheel_rect_boxy.nim b/examples/wheel_rect_boxy.nim new file mode 100644 index 0000000..39d70a0 --- /dev/null +++ b/examples/wheel_rect_boxy.nim @@ -0,0 +1,66 @@ +import + std/[strformat, times], + windy, vmath, boxy, opengl + +var + window: Window + bxy: Boxy + rectCenter: Vec2 + lastScrollAt = epochTime() + hasLastScroll = false + +const ScrollMoveScale = 1.0 + +window = newWindow("Wheel Rectangle Boxy", ivec2(1280, 800)) +window.makeContextCurrent() +loadExtensions() +bxy = newBoxy() + +rectCenter = window.size.vec2 / 2 + +echo "Use mouse wheel to move the white rectangle." +echo "Each scroll prints delta, position, and delta per second." + +proc handleScrollDelta() = + let + delta = window.scrollDelta + deltaVec = vec2(delta.x.float, delta.y.float) + now = epochTime() + if delta.x == 0 and delta.y == 0: + return + + let dt = + if hasLastScroll: + max(0.000001, now - lastScrollAt) + else: + 0.0 + + lastScrollAt = now + hasLastScroll = true + rectCenter += deltaVec * ScrollMoveScale + + let deltaPerSecond = + if dt > 0.0: + deltaVec / dt.float32 + else: + vec2(0'f32, 0'f32) + echo &"scroll delta=({deltaVec.x:.2f}, {deltaVec.y:.2f}) " & + &"delta/s=({deltaPerSecond.x:.2f}, {deltaPerSecond.y:.2f}) " & + &"position=({rectCenter.x:.2f}, {rectCenter.y:.2f})" + +window.onFrame = proc() = + bxy.beginFrame(window.size) + + let + halfSize = window.size.vec2 * 0.5 + topLeft = rectCenter - halfSize / 2 + + bxy.drawRect(rect(0, 0, window.size.x.float, window.size.y.float), color(0.08, 0.08, 0.08, 1.0)) + bxy.drawRect(rect(topLeft.x, topLeft.y, halfSize.x, halfSize.y), color(1.0, 1.0, 1.0, 1.0)) + + bxy.endFrame() + window.swapBuffers() + +while not window.closeRequested: + pollEvents() + handleScrollDelta() diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index 618c9f9..23572d8 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -563,6 +563,7 @@ proc onWheel(eventType: cint, wheelEvent: ptr EmscriptenWheelEvent, userData: po # DOM_DELTA_PIXEL (0): browsers report ~100-120px per notch on Linux. # DOM_DELTA_LINE (1): browsers report ~3 lines per notch. # DOM_DELTA_PAGE (2): one full page. + echo "wheelEvent.deltaMode: ", wheelEvent.deltaMode case wheelEvent.deltaMode of 0: # DOM_DELTA_PIXEL x *= 0.1'f32 @@ -656,17 +657,17 @@ proc handleRune(window: Window, rune: Rune) = proc windy_file_drop_callback(userData: pointer, fileNamePtr: cstring, fileDataPtr: pointer, fileDataLen: cint) {.exportc, cdecl, codegenDecl: "EMSCRIPTEN_KEEPALIVE $# $#$#".} = ## callback to handle the file drop event. ## EMSCRIPTEN_KEEPALIVE is required to avoid dead code elimination. - + let window = cast[Window](userData) if window == nil or window.onFileDrop == nil: return - + # convert the js data into Nim data. let fileName = $fileNamePtr var fileData = newString(fileDataLen) if fileDataLen > 0: copyMem(fileData[0].addr, fileDataPtr, fileDataLen) - + window.onFileDrop(fileName, fileData) proc getState(fetch: ptr emscripten_fetch_t): EmsHttpRequestState = From 5fb4242632d48743eacadd1a28a51c87279edc4c Mon Sep 17 00:00:00 2001 From: monofuel Date: Tue, 17 Feb 2026 18:46:02 -0500 Subject: [PATCH 3/6] os magnitude --- src/windy/platforms/emscripten/emdefs.nim | 9 ++++++ src/windy/platforms/emscripten/platform.nim | 33 ++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/windy/platforms/emscripten/emdefs.nim b/src/windy/platforms/emscripten/emdefs.nim index f2e0b1e..3369d28 100644 --- a/src/windy/platforms/emscripten/emdefs.nim +++ b/src/windy/platforms/emscripten/emdefs.nim @@ -181,6 +181,14 @@ EM_JS(void, set_local_storage, (const char* key, const char* value), { const valueUtf8 = UTF8ToString(value); localStorage.setItem(keyUtf8, valueUtf8); }); + +EM_JS(const char*, get_platform, (), { + var s = navigator.platform || ""; + var len = lengthBytesUTF8(s) + 1; + var buf = _malloc(len); + stringToUTF8(s, buf, len); + return buf; +}); """.} proc get_window_width*(): cint {.importc.} @@ -201,6 +209,7 @@ proc set_cursor*(cursor: cstring) {.importc.} proc getLocalStorageLength*(key: cstring): cint {.importc: "get_local_storage_length".} proc getLocalStorageInto*(output: cstring, maxLen: cint, key: cstring): cint {.importc: "get_local_storage_into".} proc setLocalStorage*(key: cstring, value: cstring) {.importc: "set_local_storage".} +proc get_platform*(): cstring {.importc.} type EMSCRIPTEN_RESULT* = cint diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index 23572d8..3ebc25c 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -48,6 +48,9 @@ type fetch*: ptr emscripten_fetch_t bodyKeepAlive*: string +let + platform = $get_platform() + var quitRequested*: bool onQuitRequest*: Callback @@ -554,29 +557,17 @@ proc onMouseMove(eventType: cint, mouseEvent: ptr EmscriptenMouseEvent, userData return 1 proc onWheel(eventType: cint, wheelEvent: ptr EmscriptenWheelEvent, userData: pointer): EM_BOOL {.cdecl.} = + ## Handle browser wheel events with OS-specific normalization. let window = cast[Window](userData) - var x = wheelEvent.deltaX.float32 - var y = wheelEvent.deltaY.float32 - - # Normalize to match native backends (~10 units per scroll notch). - # DOM_DELTA_PIXEL (0): browsers report ~100-120px per notch on Linux. - # DOM_DELTA_LINE (1): browsers report ~3 lines per notch. - # DOM_DELTA_PAGE (2): one full page. - echo "wheelEvent.deltaMode: ", wheelEvent.deltaMode - case wheelEvent.deltaMode - of 0: # DOM_DELTA_PIXEL - x *= 0.1'f32 - y *= 0.1'f32 - of 1: # DOM_DELTA_LINE - x *= 10.0'f32 / 3.0'f32 - y *= 10.0'f32 / 3.0'f32 - of 2: # DOM_DELTA_PAGE - let pageHeight = max(1'f32, window.size.y.float32 * 0.9'f32) - x *= pageHeight - y *= pageHeight - else: - discard + # macOS and Linux both report deltaMode 0 (DOM_DELTA_PIXEL) but with + # very different magnitudes. Use a flat OS-based multiplier instead. + let scale = + if "Mac" in platform: 1.0f + else: 0.2f + let + x = wheelEvent.deltaX.float32 * scale + y = wheelEvent.deltaY.float32 * scale window.state.perFrame.scrollDelta += vec2(x, y) if window.onScroll != nil: From c4e1c727d8cc68fa0b471314f243ff1afc8dd26d Mon Sep 17 00:00:00 2001 From: monofuel Date: Tue, 17 Feb 2026 18:51:36 -0500 Subject: [PATCH 4/6] neg 1 --- src/windy/platforms/emscripten/emdefs.nim | 24 ++++++++++----------- src/windy/platforms/emscripten/platform.nim | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/windy/platforms/emscripten/emdefs.nim b/src/windy/platforms/emscripten/emdefs.nim index 3369d28..1e8a294 100644 --- a/src/windy/platforms/emscripten/emdefs.nim +++ b/src/windy/platforms/emscripten/emdefs.nim @@ -91,7 +91,7 @@ EM_JS(void, setup_drag_drop_handlers_internal, (const char* target, void* userDa console.error("Canvas not found for drag and drop setup"); return; } - + // Prevent default drag behaviors on the canvas. // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API // elements do not support drop by default, you have to prevent default and stop propagation to enable drop. @@ -99,43 +99,43 @@ EM_JS(void, setup_drag_drop_handlers_internal, (const char* target, void* userDa e.preventDefault(); e.stopPropagation(); }, false); - + canvas.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); }, false); - + canvas.addEventListener('dragleave', function(e) { e.preventDefault(); e.stopPropagation(); }, false); - + // Handle the drop event. canvas.addEventListener('drop', function(e) { e.preventDefault(); e.stopPropagation(); - + if (!e.dataTransfer || !e.dataTransfer.files || e.dataTransfer.files.length === 0) { return; } - + // Process each dropped file. for (let i = 0; i < e.dataTransfer.files.length; i++) { const file = e.dataTransfer.files[i]; const reader = new FileReader(); - + reader.onload = function(evt) { if (evt.target.readyState !== FileReader.DONE) return; - + const arrayBuffer = evt.target.result; const uint8Array = new Uint8Array(arrayBuffer); - + // read the raw data from the drop event into javascript. // Allocate and copy filename. const fileNameLen = lengthBytesUTF8(file.name) + 1; const fileNamePtr = _malloc(fileNameLen); stringToUTF8(file.name, fileNamePtr, fileNameLen); - + // Allocate memory for file data. const fileDataLen = uint8Array.length; const fileDataPtr = _malloc(fileDataLen); @@ -143,12 +143,12 @@ EM_JS(void, setup_drag_drop_handlers_internal, (const char* target, void* userDa // Call the C helper function. It copies the data into Nim structures. Module._windy_file_drop_callback(userData, fileNamePtr, fileDataPtr, fileDataLen); - + // Free allocated memory. _free(fileNamePtr); _free(fileDataPtr); }; - + reader.readAsArrayBuffer(file); } }, false); diff --git a/src/windy/platforms/emscripten/platform.nim b/src/windy/platforms/emscripten/platform.nim index 3ebc25c..0e09c4c 100644 --- a/src/windy/platforms/emscripten/platform.nim +++ b/src/windy/platforms/emscripten/platform.nim @@ -563,7 +563,7 @@ proc onWheel(eventType: cint, wheelEvent: ptr EmscriptenWheelEvent, userData: po # macOS and Linux both report deltaMode 0 (DOM_DELTA_PIXEL) but with # very different magnitudes. Use a flat OS-based multiplier instead. let scale = - if "Mac" in platform: 1.0f + if "Mac" in platform: -1.0f else: 0.2f let x = wheelEvent.deltaX.float32 * scale From 8493e273ca4a255ebc5ed87380406454af9f85bd Mon Sep 17 00:00:00 2001 From: monofuel Date: Tue, 17 Feb 2026 18:52:32 -0500 Subject: [PATCH 5/6] scroll wheel example --- examples/wheel_rect_boxy.nim | 66 ------------------------------------ 1 file changed, 66 deletions(-) delete mode 100644 examples/wheel_rect_boxy.nim diff --git a/examples/wheel_rect_boxy.nim b/examples/wheel_rect_boxy.nim deleted file mode 100644 index 39d70a0..0000000 --- a/examples/wheel_rect_boxy.nim +++ /dev/null @@ -1,66 +0,0 @@ -import - std/[strformat, times], - windy, vmath, boxy, opengl - -var - window: Window - bxy: Boxy - rectCenter: Vec2 - lastScrollAt = epochTime() - hasLastScroll = false - -const ScrollMoveScale = 1.0 - -window = newWindow("Wheel Rectangle Boxy", ivec2(1280, 800)) -window.makeContextCurrent() -loadExtensions() -bxy = newBoxy() - -rectCenter = window.size.vec2 / 2 - -echo "Use mouse wheel to move the white rectangle." -echo "Each scroll prints delta, position, and delta per second." - -proc handleScrollDelta() = - let - delta = window.scrollDelta - deltaVec = vec2(delta.x.float, delta.y.float) - now = epochTime() - if delta.x == 0 and delta.y == 0: - return - - let dt = - if hasLastScroll: - max(0.000001, now - lastScrollAt) - else: - 0.0 - - lastScrollAt = now - hasLastScroll = true - rectCenter += deltaVec * ScrollMoveScale - - let deltaPerSecond = - if dt > 0.0: - deltaVec / dt.float32 - else: - vec2(0'f32, 0'f32) - echo &"scroll delta=({deltaVec.x:.2f}, {deltaVec.y:.2f}) " & - &"delta/s=({deltaPerSecond.x:.2f}, {deltaPerSecond.y:.2f}) " & - &"position=({rectCenter.x:.2f}, {rectCenter.y:.2f})" - -window.onFrame = proc() = - bxy.beginFrame(window.size) - - let - halfSize = window.size.vec2 * 0.5 - topLeft = rectCenter - halfSize / 2 - - bxy.drawRect(rect(0, 0, window.size.x.float, window.size.y.float), color(0.08, 0.08, 0.08, 1.0)) - bxy.drawRect(rect(topLeft.x, topLeft.y, halfSize.x, halfSize.y), color(1.0, 1.0, 1.0, 1.0)) - - bxy.endFrame() - window.swapBuffers() - -while not window.closeRequested: - pollEvents() - handleScrollDelta() From 00ee0d8fee66d44a53c19eccb59251b4e9873f55 Mon Sep 17 00:00:00 2001 From: monofuel Date: Tue, 17 Feb 2026 18:53:02 -0500 Subject: [PATCH 6/6] scroll wheel example --- examples/scrollwheel.nim | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 examples/scrollwheel.nim diff --git a/examples/scrollwheel.nim b/examples/scrollwheel.nim new file mode 100644 index 0000000..39d70a0 --- /dev/null +++ b/examples/scrollwheel.nim @@ -0,0 +1,66 @@ +import + std/[strformat, times], + windy, vmath, boxy, opengl + +var + window: Window + bxy: Boxy + rectCenter: Vec2 + lastScrollAt = epochTime() + hasLastScroll = false + +const ScrollMoveScale = 1.0 + +window = newWindow("Wheel Rectangle Boxy", ivec2(1280, 800)) +window.makeContextCurrent() +loadExtensions() +bxy = newBoxy() + +rectCenter = window.size.vec2 / 2 + +echo "Use mouse wheel to move the white rectangle." +echo "Each scroll prints delta, position, and delta per second." + +proc handleScrollDelta() = + let + delta = window.scrollDelta + deltaVec = vec2(delta.x.float, delta.y.float) + now = epochTime() + if delta.x == 0 and delta.y == 0: + return + + let dt = + if hasLastScroll: + max(0.000001, now - lastScrollAt) + else: + 0.0 + + lastScrollAt = now + hasLastScroll = true + rectCenter += deltaVec * ScrollMoveScale + + let deltaPerSecond = + if dt > 0.0: + deltaVec / dt.float32 + else: + vec2(0'f32, 0'f32) + echo &"scroll delta=({deltaVec.x:.2f}, {deltaVec.y:.2f}) " & + &"delta/s=({deltaPerSecond.x:.2f}, {deltaPerSecond.y:.2f}) " & + &"position=({rectCenter.x:.2f}, {rectCenter.y:.2f})" + +window.onFrame = proc() = + bxy.beginFrame(window.size) + + let + halfSize = window.size.vec2 * 0.5 + topLeft = rectCenter - halfSize / 2 + + bxy.drawRect(rect(0, 0, window.size.x.float, window.size.y.float), color(0.08, 0.08, 0.08, 1.0)) + bxy.drawRect(rect(topLeft.x, topLeft.y, halfSize.x, halfSize.y), color(1.0, 1.0, 1.0, 1.0)) + + bxy.endFrame() + window.swapBuffers() + +while not window.closeRequested: + pollEvents() + handleScrollDelta()