Skip to content
Open
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
2 changes: 1 addition & 1 deletion examples/fixedsize.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import opengl, windy

let window = newWindow("Windy Basic", ivec2(500, 500))
let window = newWindow("Windy Fixed Size", ivec2(500, 500))
window.style = Decorated

window.makeContextCurrent()
Expand Down
2 changes: 1 addition & 1 deletion examples/fullscreen.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import boxy, opengl, windy

let window = newWindow("Toggle Fullscreen", ivec2(1280, 800))
let window = newWindow("Windy Toggle Fullscreen", ivec2(1280, 800))

window.makeContextCurrent()
loadExtensions()
Expand Down
37 changes: 37 additions & 0 deletions examples/gamepad.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import opengl, windy

# Global event handlers must be registered before the first window is created,
# this enables them to receive events from gamepads that are already connected
onGamepadConnected = proc(gamepadId: int) =
echo "Gamepad ", gamepadId, " connected: ", gamepadName(gamepadId)
onGamepadDisconnected = proc(gamepadId: int) =
echo "Gamepad ", gamepadId, " disconnected"

let window = newWindow("Windy Gamepad", ivec2(1280, 800))
var color = vec4(0, 0, 0, 1)

window.makeContextCurrent()
loadExtensions()

proc gamepad() =
for i in 0..<maxGamepads:
for btn in 0.GamepadButton..<GamepadButtonCount:
if gamepadButtonPressed(i, btn):
echo "Gamepad ", i, " button ", btn, " pressed"
if gamepadButtonReleased(i, btn):
echo "Gamepad ", i, " button ", btn, " released"
if gamepadButtonPressure(i, btn) > 0 and gamepadButtonPressure(i, btn) < 1:
echo "Gamepad ", i, " button ", btn, " pressure ", gamepadButtonPressure(i, btn)
for axis in 0.GamepadAxis..<GamepadAxisCount:
if gamepadAxis(i, axis) != 0:
echo "Gamepad ", i, " axis ", axis, " value ", gamepadAxis(i, axis)

proc display() =
glClearColor(color.x, color.y, color.z, color.w)
glClear(GL_COLOR_BUFFER_BIT)
window.swapBuffers()

while not window.closeRequested:
gamepad()
display()
pollEvents()
2 changes: 1 addition & 1 deletion examples/httprequest.nim
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ req.onResponse = proc(response: HttpResponse) =
echo "onResponse: code=", $response.code, ", len=", response.body.len

# Closing the window exits the demo
let window = newWindow("Windy Basic", ivec2(1280, 800))
let window = newWindow("Windy HttpRequest", ivec2(1280, 800))
while not window.closeRequested:
pollEvents()
2 changes: 1 addition & 1 deletion examples/websocket.nim
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ ws.onClose = proc() =
echo "onClose"

# Closing the window exits the demo
let window = newWindow("Windy Basic", ivec2(1280, 800))
let window = newWindow("Windy WebSocket", ivec2(1280, 800))
while not window.closeRequested:
pollEvents()
38 changes: 38 additions & 0 deletions src/windy/common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type
DecoratedResizable, Decorated, Undecorated, Transparent

Callback* = proc()
GamepadCallback* = proc(gamepadId: int) {.raises: [].}
ButtonCallback* = proc(button: Button)
RuneCallback* = proc(rune: Rune)
HttpErrorCallback* = proc(msg: string)
Expand Down Expand Up @@ -194,9 +195,46 @@ type

ButtonView* = distinct set[Button]

# A button is an input whose value is a boolean with an optional pressure value between 0 and 1
# For buttons without pressure, the pressure value is 1 if the button is pressed, 0 otherwise
# NOTE These are the same as the HTML5 standard mapping, making the definition order important
GamepadButton* = enum
GamepadA
GamepadB
GamepadX
GamepadY
GamepadL1
GamepadR1
GamepadL2
GamepadR2
GamepadSelect
GamepadStart
GamepadL3
GamepadR3
GamepadUp
GamepadDown
GamepadLeft
GamepadRight
GamepadHome
GamepadTouchpad
GamepadButtonCount

# An axis is an input whose value is a float between -1 and 1
GamepadAxis* = enum
GamepadLStickX
GamepadLStickY
GamepadRStickX
GamepadRStickY
GamepadAxisCount

const
maxGamepads* = 4 # GCController, XInput and other native APIs come with a limit of 4 gamepads
defaultHttpDeadline*: float32 = -1

var
onGamepadConnected*: GamepadCallback
onGamepadDisconnected*: GamepadCallback

proc `==`*(a, b: HttpRequestHandle): bool =
a.int == b.int

Expand Down
47 changes: 47 additions & 0 deletions src/windy/internal.nim
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import common, pixie, std/random

const
gamepadDeadzone = 0.1
multiClickRadius = 4
CRLF* = "\r\n"

type
GamepadState* = object
buttons*: uint32 # One bit per button, 32 buttons max
pressed*: uint32 # Buttons pressed this frame
released*: uint32 # Buttons released this frame
pressures*: array[GamepadButtonCount.int, float32] # Many APIs report a pressure per button; binary 0..1 otherwise
axes*: array[GamepadAxisCount.int, float32] # Values are in the range -1..1
name*: string

WindowState* = object
title*: string
icon*: Image
Expand Down Expand Up @@ -162,3 +171,41 @@ proc addDefaultHeaders*(headers: var seq[HttpHeader]) =
if headers["accept-encoding"].len == 0:
# If there isn't a specific accept-encoding specified, enable gzip
headers["accept-encoding"] = "gzip"

template gamepadPlatform*() =
proc gamepadName*(gamepadId: int): string =
gamepadStates[gamepadId].name

proc gamepadButton*(gamepadId: int, button: GamepadButton): bool {.inline.} =
(gamepadStates[gamepadId].buttons and (1.uint32 shl button.int8)) != 0

proc gamepadButtonPressed*(gamepadId: int, button: GamepadButton): bool {.inline.} =
(gamepadStates[gamepadId].pressed and (1.uint32 shl button.int8)) != 0

proc gamepadButtonReleased*(gamepadId: int, button: GamepadButton): bool {.inline.} =
(gamepadStates[gamepadId].released and (1.uint32 shl button.int8)) != 0

proc gamepadButtonPressure*(gamepadId: int, button: GamepadButton): float {.inline.} =
gamepadStates[gamepadId].pressures[button.int8]

proc gamepadAxis*(gamepadId: int, axis: GamepadAxis): float {.inline.} =
gamepadStates[gamepadId].axes[axis.int8]

func gamepadFilterDeadZone*(value: float): float {.inline.} =
if abs(value) < gamepadDeadzone: 0 else: value

template gamepadUpdateButtons*() =
let prevButtons = state.buttons
state.buttons = buttons
state.pressed = buttons and (not prevButtons)
state.released = prevButtons and (not buttons)

proc gamepadResetState*(state: var GamepadState) =
state.buttons = 0.uint32
state.pressed = 0.uint32
state.released = 0.uint32
for i in 0..<GamepadButtonCount.int:
state.pressures[i] = 0.float32
for i in 0..<GamepadAxisCount.int:
state.axes[i] = 0.float32
state.name = ""
21 changes: 21 additions & 0 deletions src/windy/platforms/emscripten/emdefs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,22 @@ proc emscripten_webgl_init_context_attributes*(attrs: ptr EmscriptenWebGLContext
proc emscripten_webgl_create_context*(target: cstring, attrs: ptr EmscriptenWebGLContextAttributes): EMSCRIPTEN_WEBGL_CONTEXT_HANDLE {.importc, header: "<emscripten/html5.h>".}
proc emscripten_webgl_make_context_current*(context: EMSCRIPTEN_WEBGL_CONTEXT_HANDLE): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}

const EM_HTML5_MEDIUM_STRING_LEN_BYTES* = 64

# Mouse event handling
type
EmscriptenGamepadEvent* {.importc: "EmscriptenGamepadEvent", header: "<emscripten/html5.h>".} = object
timestamp*: cdouble
numAxes*: cint
numButtons*: cint
axis*: array[64, cdouble]
analogButton*: array[64, cdouble]
digitalButton*: array[64, bool]
connected*: bool
index*: cint
id*: array[EM_HTML5_MEDIUM_STRING_LEN_BYTES, char]
mapping*: array[EM_HTML5_MEDIUM_STRING_LEN_BYTES, char]

EmscriptenMouseEvent* {.importc: "EmscriptenMouseEvent", header: "<emscripten/html5.h>".} = object
timestamp*: cdouble
screenX*, screenY*: clong
Expand Down Expand Up @@ -181,7 +195,11 @@ type
EmscriptenKeyboardEventCallback* = proc(eventType: cint, keyEvent: ptr EmscriptenKeyboardEvent, userData: pointer): EM_BOOL {.cdecl.}
EmscriptenFocusEventCallback* = proc(eventType: cint, focusEvent: ptr EmscriptenFocusEvent, userData: pointer): EM_BOOL {.cdecl.}
EmscriptenUiEventCallback* = proc(eventType: cint, uiEvent: ptr EmscriptenUiEvent, userData: pointer): EM_BOOL {.cdecl.}
EmscriptenGamepadEventCallback* = proc(eventType: cint, gamepadEvent: ptr EmscriptenGamepadEvent, userData: pointer): EM_BOOL {.cdecl.}
EmscriptenGamepadDisconnectedEventCallback* = proc(eventType: cint, gamepadEvent: ptr EmscriptenGamepadEvent, userData: pointer): EM_BOOL {.cdecl.}

proc emscripten_set_gamepadconnected_callback_on_thread*(userData: pointer, useCapture: EM_BOOL, callback: EmscriptenGamepadEventCallback, targetThread: pointer): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}
proc emscripten_set_gamepaddisconnected_callback_on_thread*(userData: pointer, useCapture: EM_BOOL, callback: EmscriptenGamepadDisconnectedEventCallback, targetThread: pointer): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}
proc emscripten_set_mousedown_callback_on_thread*(target: cstring, userData: pointer, useCapture: EM_BOOL, callback: EmscriptenMouseEventCallback, targetThread: pointer): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}
proc emscripten_set_mouseup_callback_on_thread*(target: cstring, userData: pointer, useCapture: EM_BOOL, callback: EmscriptenMouseEventCallback, targetThread: pointer): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}
proc emscripten_set_mousemove_callback_on_thread*(target: cstring, userData: pointer, useCapture: EM_BOOL, callback: EmscriptenMouseEventCallback, targetThread: pointer): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}
Expand All @@ -193,6 +211,9 @@ proc emscripten_set_blur_callback_on_thread*(target: cstring, userData: pointer,
proc emscripten_set_focus_callback_on_thread*(target: cstring, userData: pointer, useCapture: EM_BOOL, callback: EmscriptenFocusEventCallback, targetThread: pointer): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}
proc emscripten_set_resize_callback_on_thread*(target: cstring, userData: pointer, useCapture: EM_BOOL, callback: EmscriptenUiEventCallback, targetThread: pointer): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}

proc emscripten_sample_gamepad_data*(): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}
proc emscripten_get_gamepad_status*(index: cint, status: ptr EmscriptenGamepadEvent): EMSCRIPTEN_RESULT {.importc, header: "<emscripten/html5.h>".}

const
EMSCRIPTEN_EVENT_KEYPRESS* = 1
EMSCRIPTEN_EVENT_KEYDOWN* = 2
Expand Down
62 changes: 62 additions & 0 deletions src/windy/platforms/emscripten/platform.nim
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ var
mainWindow: Window # Track the main window for events
httpRequests: Table[HttpRequestHandle, EmsHttpRequestState]

gamepadsConnectedMask: uint8
gamepadStates: array[maxGamepads, GamepadState]

proc handleButtonPress(window: Window, button: Button)
proc handleButtonRelease(window: Window, button: Button)
proc handleRune(window: Window, rune: Rune)
proc setupEventHandlers(window: Window) # Forward declaration
proc setupGamepads()

proc init =
if initialized:
Expand Down Expand Up @@ -242,6 +246,7 @@ proc newWindow*(

# Setup event handlers
setupEventHandlers(result)
setupGamepads()

result.title = title
if pos != ivec2(0, 0):
Expand Down Expand Up @@ -462,6 +467,59 @@ proc keyCodeToButton(keyCode: culong): Button =
of 222: KeyApostrophe
else: ButtonUnknown

gamepadPlatform()

proc gamepadConnected*(gamepadId: int): bool =
(gamepadsConnectedMask and (1.uint8 shl gamepadId)) != 0

proc strcmp(a: cstring, b: cstring): cint {.importc, header: "<string.h>".}

proc onGamepadConnected(eventType: cint, gamepadEvent: ptr EmscriptenGamepadEvent, userData: pointer): EM_BOOL {.cdecl.} =
# We can only ensure known stable mappings if the gamepad reports the standard mapping
if strcmp(cast[cstring](addr gamepadEvent.mapping), cstring "standard") == 0:
gamepadsConnectedMask = gamepadsConnectedMask or (1.uint8 shl gamepadEvent.index)
gamepadStates[gamepadEvent.index].name = $gamepadEvent.id
if common.onGamepadConnected != nil:
common.onGamepadConnected(gamepadEvent.index)
return 1

proc onGamepadDisconnected(eventType: cint, gamepadEvent: ptr EmscriptenGamepadEvent, userData: pointer): EM_BOOL {.cdecl.} =
if (gamepadsConnectedMask and (1.uint8 shl gamepadEvent.index)) != 0:
gamepadsConnectedMask = gamepadsConnectedMask and (not (1.uint8 shl gamepadEvent.index))
gamepadResetState(gamepadStates[gamepadEvent.index])
if common.onGamepadDisconnected != nil:
common.onGamepadDisconnected(gamepadEvent.index)
return 1

proc setupGamepads() =
discard emscripten_sample_gamepad_data() # Populate gamepad status data we're about to read

var gp: EmscriptenGamepadEvent
for i in 0..<maxGamepads:
if emscripten_get_gamepad_status(cint i, addr gp) == 0 and gp.connected:
discard onGamepadConnected(0, addr gp, nil)

proc pollGamepads() =
discard emscripten_sample_gamepad_data()

var gp: EmscriptenGamepadEvent
for i in 0..<maxGamepads:
if (gamepadsConnectedMask and (1.uint8 shl i)) == 0:
continue

discard emscripten_get_gamepad_status(cint i, addr gp)

var state = addr gamepadStates[i]
var buttons = uint32 0
for j in 0..<GamepadButtonCount.int:
state.pressures[j] = gp.analogButton[j]
if gp.digitalButton[j]:
buttons = buttons or (uint32 1 shl j)
for j in 0..<GamepadAxisCount.int:
state.axes[j] = gp.axis[j]

gamepadUpdateButtons()

proc mouseButtonToButton(button: cushort): Button =
case button:
of 0: MouseLeft
Expand Down Expand Up @@ -554,6 +612,10 @@ proc onCanvasResize(userData: pointer) {.cdecl, exportc.} =
window.onResize()

proc setupEventHandlers(window: Window) =
# Gamepad events
discard emscripten_set_gamepadconnected_callback_on_thread(nil, 1, onGamepadConnected, EM_CALLBACK_THREAD_CONTEXT)
discard emscripten_set_gamepaddisconnected_callback_on_thread(nil, 1, onGamepadDisconnected, EM_CALLBACK_THREAD_CONTEXT)

# 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)
Expand Down
Loading