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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:

# Run tests.
- run: nim c tests/test.nim
- run: nim c tests/manual_inputs.nim
- run: nim r tests/test_openurl.nim
- run: nim r tests/test_macos.nim
if: matrix.os == 'macos-latest'
Expand Down Expand Up @@ -64,6 +65,8 @@ jobs:
if: matrix.os == 'ubuntu-latest'

# Build Emscripten examples on Ubuntu only.
- run: nim c -d:emscripten tests/manual_inputs.nim
if: matrix.os == 'ubuntu-latest'
- run: nim c -d:emscripten examples/basic.nim
if: matrix.os == 'ubuntu-latest'
- run: nim c -d:emscripten examples/basic_boxy.nim
Expand Down
42 changes: 42 additions & 0 deletions docs/browser_inputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Manual browser input check

Use `tests/manual_inputs.nim` to compare native and browser input.
Received text appears in the window or tab title. The console logs every
callback and the keys held when it runs.

Run the native test from the repository root:

```sh
nim r tests/manual_inputs.nim
```

Compile with the shared examples configuration, then open it with Emscripten:

```sh
nim c -d:emscripten tests/manual_inputs.nim
emrun examples/emscripten/manual_inputs.html
```

Open the browser console, then click the canvas. The background turns red
while a modifier is held, flashes green for received text, and turns blue for
other held keys. F2 toggles rune input and Escape clears the displayed text.

- Type `wasd`, spaces, uppercase letters, and characters from your keyboard
layout. Each character should appear once in the title and as one `onRune`
callback. Physical key callbacks should identify the key independently of
the character it produces.
- Hold a letter to repeat it. Each repeat should produce a press and a rune.
- Turn rune input off with F2. Keys should still report presses and releases,
but no text should be added. Turn it back on and check typing again.
- Quickly press and release Control-A or Command-A. The A callbacks should
show the modifier held, and the shortcut should not append text to the title.
- Try left and right Shift, Control, Alt, and Command/Super. Check their names
and held state in the log. Test both Shift keys held together and release
only one. The other should remain held.
- Try arrows, function keys, and numpad keys. They should report their own
buttons; navigation keys should not generate text or move browser focus.
- Hold a key and switch away from the window. Releases should occur before
the focus-change callback. Return and check that no keys remain stuck.

These checks intentionally use real browser events. Compiling an example in
CI does not verify browser event delivery.
161 changes: 153 additions & 8 deletions src/windy/platforms/emscripten/platform.nim
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ proc loadExtensions*() =

# Convert JavaScript key codes to windy Button enum
proc keyCodeToButton(keyCode: culong): Button =
## Maps legacy key codes when the browser has no physical key code.
case keyCode:
of 27: KeyEscape
of 32: KeySpace
Expand Down Expand Up @@ -572,6 +573,8 @@ proc keyCodeToButton(keyCode: culong): Button =
of 88: KeyX
of 89: KeyY
of 90: KeyZ
of 91, 92: KeyLeftSuper
of 93: KeyRightSuper
of 48: Key0
of 49: Key1
of 50: Key2
Expand All @@ -595,6 +598,120 @@ proc keyCodeToButton(keyCode: culong): Button =
of 222: KeyApostrophe
else: ButtonUnknown

proc keyEventToButton(event: ptr EmscriptenKeyboardEvent): Button =
## Maps physical browser keys independently of the keyboard layout.
let code = $cast[cstring](addr event.code[0])
if code.len == 4 and code.startsWith("Key") and code[3] in 'A' .. 'Z':
return Button(KeyA.ord + code[3].ord - 'A'.ord)
if code.len == 6 and code.startsWith("Digit") and code[5] in '0' .. '9':
return Button(Key0.ord + code[5].ord - '0'.ord)
if code.len == 7 and code.startsWith("Numpad") and code[6] in '0' .. '9':
return Button(Numpad0.ord + code[6].ord - '0'.ord)
case code
of "Backquote": KeyBacktick
of "Minus": KeyMinus
of "Equal": KeyEqual
of "Backspace": KeyBackspace
of "Tab": KeyTab
of "BracketLeft": KeyLeftBracket
of "BracketRight": KeyRightBracket
of "Backslash", "IntlBackslash": KeyBackslash
of "CapsLock": KeyCapsLock
of "Semicolon": KeySemicolon
of "Quote": KeyApostrophe
of "Enter": KeyEnter
of "ShiftLeft": KeyLeftShift
of "Comma": KeyComma
of "Period": KeyPeriod
of "Slash": KeySlash
of "ShiftRight": KeyRightShift
of "ControlLeft": KeyLeftControl
of "MetaLeft", "OSLeft": KeyLeftSuper
of "AltLeft": KeyLeftAlt
of "Space": KeySpace
of "AltRight": KeyRightAlt
of "MetaRight", "OSRight": KeyRightSuper
of "ContextMenu": KeyMenu
of "ControlRight": KeyRightControl
of "Delete": KeyDelete
of "Home": KeyHome
of "End": KeyEnd
of "Insert": KeyInsert
of "PageUp": KeyPageUp
of "PageDown": KeyPageDown
of "Escape": KeyEscape
of "ArrowUp": KeyUp
of "ArrowDown": KeyDown
of "ArrowLeft": KeyLeft
of "ArrowRight": KeyRight
of "PrintScreen": KeyPrintScreen
of "ScrollLock": KeyScrollLock
of "Pause": KeyPause
of "F1": KeyF1
of "F2": KeyF2
of "F3": KeyF3
of "F4": KeyF4
of "F5": KeyF5
of "F6": KeyF6
of "F7": KeyF7
of "F8": KeyF8
of "F9": KeyF9
of "F10": KeyF10
of "F11": KeyF11
of "F12": KeyF12
of "NumLock": KeyNumLock
of "NumpadDecimal": NumpadDecimal
of "NumpadEnter": NumpadEnter
of "NumpadAdd": NumpadAdd
of "NumpadSubtract": NumpadSubtract
of "NumpadMultiply": NumpadMultiply
of "NumpadDivide": NumpadDivide
of "NumpadEqual": NumpadEqual
else:
# Android browsers can omit event.code despite supporting WebGL2.
let button = keyCodeToButton(event.keyCode)
if event.location == 2:
case button
of KeyLeftShift: return KeyRightShift
of KeyLeftControl: return KeyRightControl
of KeyLeftAlt: return KeyRightAlt
of KeyLeftSuper: return KeyRightSuper
else: discard
button

proc syncModifiers(
window: Window,
event: ptr EmscriptenKeyboardEvent,
button = ButtonUnknown,
pressed = false
) =
## Makes the event's modifier state visible before any callbacks run.
let previous = window.state.buttonDown
for (left, right, held) in [
(KeyLeftControl, KeyRightControl, event.ctrlKey),
(KeyLeftShift, KeyRightShift, event.shiftKey),
(KeyLeftAlt, KeyRightAlt, event.altKey),
(KeyLeftSuper, KeyRightSuper, event.metaKey)
]:
if button in {left, right}:
if pressed:
window.state.buttonDown.incl(button)
else:
window.state.buttonDown.excl(button)
if not held:
window.state.buttonDown.excl(left)
window.state.buttonDown.excl(right)
elif left notin window.state.buttonDown and
right notin window.state.buttonDown:
window.state.buttonDown.incl(left)
let current = window.state.buttonDown
for changed in previous - current:
if changed != button:
window.handleButtonRelease(changed)
for changed in current - previous:
if changed != button:
window.handleButtonPress(changed)

proc mouseButtonToButton(button: cushort): Button =
case button:
of 0: MouseLeft
Expand Down Expand Up @@ -657,21 +774,47 @@ proc onWheel(eventType: cint, wheelEvent: ptr EmscriptenWheelEvent, userData: po
return 1

proc onKeyDown(eventType: cint, keyEvent: ptr EmscriptenKeyboardEvent, userData: pointer): EM_BOOL {.cdecl.} =
let window = cast[Window](userData)
let button = keyCodeToButton(keyEvent.keyCode)
window.handleButtonPress(button)
## Delivers key presses and lets printable keys produce a text event.
let
window = cast[Window](userData)
button = keyEventToButton(keyEvent)
key = $cast[cstring](addr keyEvent.key[0])
window.syncModifiers(keyEvent, button, pressed = true)
if button != ButtonUnknown:
window.handleButtonPress(button)
if window.runeInputEnabled and not keyEvent.metaKey and
(not keyEvent.ctrlKey or keyEvent.altKey) and
(key.runeLen == 1 or key == "Dead" or key == "Process"):
# Cancelling keydown also suppresses the browser's keypress event.
return 0
return 1

proc onKeyUp(eventType: cint, keyEvent: ptr EmscriptenKeyboardEvent, userData: pointer): EM_BOOL {.cdecl.} =
let window = cast[Window](userData)
let button = keyCodeToButton(keyEvent.keyCode)
window.handleButtonRelease(button)
## Delivers releases with up-to-date modifier state.
let
window = cast[Window](userData)
button = keyEventToButton(keyEvent)
window.syncModifiers(keyEvent, button)
if button != ButtonUnknown:
window.handleButtonRelease(button)
if button in {KeyLeftSuper, KeyRightSuper} and not keyEvent.metaKey:
# Browsers can omit releases for keys used in Command shortcuts.
let held = window.state.buttonDown - {
KeyLeftControl, KeyRightControl, KeyLeftShift, KeyRightShift,
KeyLeftAlt, KeyRightAlt, KeyLeftSuper, KeyRightSuper
}
for released in held:
if released >= Key0:
window.handleButtonRelease(released)
return 1

proc onKeyPress(eventType: cint, keyEvent: ptr EmscriptenKeyboardEvent, userData: pointer): EM_BOOL {.cdecl.} =
## Delivers text once through the shared desktop rune handler.
let window = cast[Window](userData)
if keyEvent.charCode > 0:
window.handleRune(Rune(keyEvent.charCode))
window.syncModifiers(keyEvent)
if not keyEvent.metaKey and
(not keyEvent.ctrlKey or keyEvent.altKey) and keyEvent.charCode > 0:
window.handleRune(Rune(keyEvent.charCode))
return 1

proc onFocus(eventType: cint, focusEvent: ptr EmscriptenFocusEvent, userData: pointer): EM_BOOL {.cdecl.} =
Expand All @@ -684,7 +827,9 @@ proc onFocus(eventType: cint, focusEvent: ptr EmscriptenFocusEvent, userData: po
return 1

proc onBlur(eventType: cint, focusEvent: ptr EmscriptenFocusEvent, userData: pointer): EM_BOOL {.cdecl.} =
## Releases held input before notifying the application of focus loss.
let window = cast[Window](userData)
clearButtonsTemplate()
if frameDispatchActive:
pendingFocusChange = true
return 1
Expand Down
3 changes: 3 additions & 0 deletions tests/config.nims
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
--path:"../src"

when defined(emscripten):
include "../examples/config.nims"
79 changes: 79 additions & 0 deletions tests/manual_inputs.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import
std/[os, times, unicode],
opengl, windy

const Modifiers = {
KeyLeftControl, KeyRightControl, KeyLeftShift, KeyRightShift,
KeyLeftAlt, KeyRightAlt, KeyLeftSuper, KeyRightSuper
}

let window = newWindow("Windy Inputs", ivec2(1000, 600))

var
text: seq[Rune]
lastRuneTime: float64

proc updateTitle() =
## Shows text and the current held keys without needing a font asset.
let mode =
if window.runeInputEnabled:
"on"
else:
"off"
window.title = "F2 text=" & mode & ", Esc clears | " & $text &
" | held: " & $set[Button](window.buttonDown)

window.runeInputEnabled = true
window.makeContextCurrent()
loadExtensions()
updateTitle()

window.onButtonPress = proc(button: Button) =
## Logs the key and modifiers as seen by a normal application callback.
echo "press ", button, " held: ", set[Button](window.buttonDown)
case button
of KeyF2:
window.runeInputEnabled = not window.runeInputEnabled
of KeyEscape:
text.setLen(0)
else:
discard
updateTitle()

window.onButtonRelease = proc(button: Button) =
## Logs releases and updates the displayed held state.
echo "release ", button, " held: ", set[Button](window.buttonDown)
updateTitle()

window.onRune = proc(rune: Rune) =
## Displays each received rune and flashes the window green.
echo "rune ", rune, " held: ", set[Button](window.buttonDown)
text.add(rune)
if text.len > 80:
text.delete(0)
lastRuneTime = epochTime()
updateTitle()

window.onFocusChange = proc() =
## Makes stuck input visible after switching away and returning.
echo "focus ", window.focused, " held: ", set[Button](window.buttonDown)
updateTitle()

window.onFrame = proc() =
## Shows modifiers in red, received text in green, and other keys in blue.
let held = set[Button](window.buttonDown)
if (held * Modifiers) != {}:
glClearColor(0.35f, 0.08f, 0.08f, 1.0f)
elif epochTime() - lastRuneTime < 0.2:
glClearColor(0.08f, 0.35f, 0.12f, 1.0f)
elif (held * {Key0 .. Button.high}) != {}:
glClearColor(0.08f, 0.15f, 0.35f, 1.0f)
else:
glClearColor(0.08f, 0.08f, 0.08f, 1.0f)
glClear(GL_COLOR_BUFFER_BIT)
window.swapBuffers()

while not window.closeRequested:
if window.minimized or not window.visible:
sleep(10)
pollEvents()
Loading