begin refactoring shell/glfw-gamepad.sml. (Most functions inside that file are pure now, and I think a 'modal' approach to the face buttons would be better/more ergonomic.)

This commit is contained in:
2026-01-21 20:38:13 +00:00
parent 96fe9cea0d
commit 0679f4154e
2 changed files with 171 additions and 240 deletions

View File

@@ -129,7 +129,6 @@ struct
loop (0, DrawMailbox.getMessagesAndClear (), shellState) loop (0, DrawMailbox.getMessagesAndClear (), shellState)
end end
local
fun updateLoop (pos, msgVec, app) = fun updateLoop (pos, msgVec, app) =
if pos = Vector.length msgVec then if pos = Vector.length msgVec then
app app
@@ -140,13 +139,12 @@ struct
in in
updateLoop (pos + 1, msgVec, app) updateLoop (pos + 1, msgVec, app)
end end
in
fun update app = fun update app =
updateLoop (0, InputMailbox.getMessagesAndClear (), app) updateLoop (0, InputMailbox.getMessagesAndClear (), app)
end
fun helpLoop (app, shellState as {window, ...}: t) = fun helpLoop (app, shellState: t, gamepad) =
case Glfw.windowShouldClose window of case Glfw.windowShouldClose (#window shellState) of
false => false =>
let let
val shellState = consumeDrawEvents shellState val shellState = consumeDrawEvents shellState
@@ -154,19 +152,34 @@ struct
val _ = Gles3.clearColor (0.89, 0.89, 0.89, 1.0) val _ = Gles3.clearColor (0.89, 0.89, 0.89, 1.0)
val _ = Gles3.clear () val _ = Gles3.clear ()
val () = GlfwGamepad.query () (* one update reacting to gamepad events *)
val (gamepad, actions) = GlfwGamepad.query gamepad
val app = updateLoop (0, Vector.fromList actions, app)
(* one update reacting to keyboard events *)
val app = update app val app = update app
val _ = draw shellState val _ = draw shellState
val _ = Glfw.swapBuffers window val _ = Glfw.swapBuffers (#window shellState)
val _ = Glfw.waitEvents () val _ = Glfw.waitEvents ()
in in
helpLoop (app, shellState) helpLoop (app, shellState, gamepad)
end end
| true => Glfw.terminate () | true => Glfw.terminate ()
fun loop (app, window) = fun loop (app, window) =
let val shellState = create window let
in helpLoop (app, shellState) val shellState = create window
val gamepad: GlfwGamepad.gamepad_state =
{ mode = GlfwGamepad.PENDING
, shiftChr = false
, trianglePressed = false
, circlePressed = false
, crossPressed = false
, squarePressed = false
}
in
helpLoop (app, shellState, gamepad)
end end
end end

View File

@@ -1,33 +1,127 @@
structure GlfwGamepad = structure GlfwGamepad =
struct struct
datatype joystick_dir = UP | LEFT | DOWN | RIGHT | CENTRE | L2 | R2 datatype mode =
PENDING
| TRIANGLE
| TRIANGLE_CIRCLE
| CIRCLE
| CIRCLE_CROSS
| CROSS
| CROSS_SQUARE
| SQUARE
(* maybe SQUARE_TRIANGLE for numbers and symbols? *)
| SQUARE_TRIANGLE
fun isInDeadZone (x, y) = structure IM = InputMsg
x < 0.1 andalso x > ~0.1 andalso y < 0.1 andalso y > ~0.1
fun axisToDir (x, y, l2, r2) = type gamepad_state =
if isInDeadZone (x, y) then { mode: mode
(* analogue is in dead zone, so only query L2 and R2 *) , shiftChr: bool
if r2 < 0.3 andalso l2 < 0.3 then CENTRE , trianglePressed: bool
else if abs r2 > abs l2 then R2 , circlePressed: bool
else L2 , crossPressed: bool
else , squarePressed: bool
}
fun onPendingMode
( state: gamepad_state
, trianglePressed
, circlePressed
, crossPressed
, squarePressed
, actions: IM.t list
) =
if
trianglePressed orelse circlePressed orelse crossPressed
orelse squarePressed
then
(* some button is being pressed,
* so we record that in the returned state,
* in addition to whatever buttons were previously pressed *)
let let
val ax = abs x val trianglePressed = #trianglePressed state orelse trianglePressed
val ay = abs y val circlePressed = #circlePressed state orelse circlePressed
val al2 = abs l2 val crossPressed = #crossPressed state orelse crossPressed
val ar2 = abs r2 val squarePressed = #squarePressed state orelse squarePressed
val newState =
{ mode = #mode state
, shiftChr = #shiftChr state
, trianglePressed = trianglePressed
, circlePressed = circlePressed
, crossPressed = crossPressed
, squarePressed = squarePressed
}
in in
if ax > ay andalso ax > al2 andalso ax > ar2 then (newState, actions)
if x > 0.0 then RIGHT else LEFT end
else if ay > ax andalso ay > al2 andalso ay > ar2 then
if y > 0.0 then DOWN else UP
else if al2 > ax andalso al2 > ay andalso al2 > ar2 then
L2
else else
R2 (* nothing is currently pressed,
* so we check if there is a valid mode indicated by the state
* and change the mode if so *)
let
val
{ trianglePressed
, circlePressed
, crossPressed
, squarePressed
, shiftChr
, mode = _
} = state
val newMode =
if trianglePressed andalso not (crossPressed orelse squarePressed) then
if not circlePressed then TRIANGLE else TRIANGLE_CIRCLE
else if circlePressed andalso not squarePressed then
if not crossPressed then CIRCLE else CIRCLE_CROSS
else if crossPressed then
if not squarePressed then CROSS else CROSS_SQUARE
else if squarePressed then
if not trianglePressed then SQUARE else SQUARE_TRIANGLE
else
(* some buttons are being pressed,
* but not a valid combination to switch
* to another mode, so we are still on PENDING *)
PENDING
val newState =
{ mode = newMode
, shiftChr = shiftChr
, trianglePressed = false
, circlePressed = false
, crossPressed = false
, squarePressed = false
}
in
(newState, actions)
end end
fun handleButtons
( gamepadState
, trianglePressed
, circlePressed
, crossPressed
, squarePressed
, l1Pressed
, r1Pressed
) =
let
val actions = if l1Pressed then [IM.KEY_BACKSPACE] else []
val actions =
if r1Pressed then (IM.CHAR_EVENT #" ") :: actions else actions
in
case #mode gamepadState of
PENDING =>
onPendingMode
( gamepadState
, trianglePressed
, circlePressed
, crossPressed
, squarePressed
, actions
)
end
(* impure functions below *)
fun getGamepadState () = fun getGamepadState () =
Input.getGamepadState 0 <> 0 orelse Input.getGamepadState 1 <> 0 Input.getGamepadState 0 <> 0 orelse Input.getGamepadState 1 <> 0
orelse Input.getGamepadState 2 <> 0 orelse Input.getGamepadState 3 <> 0 orelse Input.getGamepadState 2 <> 0 orelse Input.getGamepadState 3 <> 0
@@ -38,203 +132,27 @@ struct
orelse Input.getGamepadState 12 <> 0 orelse Input.getGamepadState 13 <> 0 orelse Input.getGamepadState 12 <> 0 orelse Input.getGamepadState 13 <> 0
orelse Input.getGamepadState 14 <> 0 orelse Input.getGamepadState 15 <> 0 orelse Input.getGamepadState 14 <> 0 orelse Input.getGamepadState 15 <> 0
local fun query gamepadState =
val state = if getGamepadState () then
{ crossPressed = ref false
, circlePressed = ref false
, squarePressed = ref false
, trianglePressed = ref false
, r1Pressed = ref false
, l1Pressed = ref false
, shiftChr = ref false
}
fun appendChr chr =
(InputMailbox.append (InputMsg.CHAR_EVENT chr); #shiftChr state := false)
fun handleTrianglePressed (x, y, l2, r2) =
if !(#trianglePressed state) then
()
else
let let
val () = #trianglePressed state := true val trianglePressed = Input.isTriangleButtonPressed () <> 0
val chr = val circlePressed = Input.isCircleButtonPressed () <> 0
if !(#shiftChr state) then val crossPressed = Input.isCrossButtonPressed () <> 0
case axisToDir (x, y, l2, r2) of val squarePressed = Input.isSquareButtonPressed () <> 0
CENTRE => #"A" val l1Pressed = Input.isL1ButtonPressed () <> 0
| UP => #"E" val r1Pressed = Input.isL1ButtonPressed () <> 0
| RIGHT => #"I"
| DOWN => #"M"
| LEFT => #"Q"
| L2 => #"U"
| R2 => #"Y"
else
case axisToDir (x, y, l2, r2) of
CENTRE => #"a"
| UP => #"e"
| RIGHT => #"i"
| DOWN => #"m"
| LEFT => #"q"
| L2 => #"u"
| R2 => #"y"
in in
appendChr chr handleButtons
( gamepadState
, trianglePressed
, circlePressed
, crossPressed
, squarePressed
, l1Pressed
, r1Pressed
)
end end
fun handleCirclePressed (x, y, l2, r2) =
if !(#circlePressed state) then
()
else else
let (* nothing to do, so return same state *)
val () = #circlePressed state := true (gamepadState, [])
val chr =
if !(#shiftChr state) then
case axisToDir (x, y, l2, r2) of
CENTRE => #"B"
| UP => #"F"
| RIGHT => #"J"
| DOWN => #"N"
| LEFT => #"R"
| L2 => #"V"
| R2 => #"Z"
else
case axisToDir (x, y, l2, r2) of
CENTRE => #"b"
| UP => #"f"
| RIGHT => #"j"
| DOWN => #"n"
| LEFT => #"r"
| L2 => #"v"
| R2 => #"z"
in
appendChr chr
end
fun handleCrossPressed (x, y, l2, r2) =
if !(#crossPressed state) then
()
else
let
val () = #crossPressed state := true
in
if !(#shiftChr state) then
case axisToDir (x, y, l2, r2) of
CENTRE => appendChr #"U"
| UP => appendChr #"U"
| RIGHT => appendChr #"U"
| DOWN => appendChr #"U"
| LEFT => appendChr #"U"
| L2 => appendChr #"U"
| R2 => InputMailbox.append InputMsg.KEY_ENTER
else
case axisToDir (x, y, l2, r2) of
CENTRE => appendChr #"c"
| UP => appendChr #"g"
| RIGHT => appendChr #"k"
| DOWN => appendChr #"o"
| LEFT => appendChr #"s"
| L2 => appendChr #"w"
| R2 => InputMailbox.append InputMsg.KEY_ENTER
end
fun handleSquarePressed (x, y, l2, r2) =
if !(#squarePressed state) then
()
else
let
val () = #squarePressed state := true
in
if !(#shiftChr state) then
case axisToDir (x, y, l2, r2) of
CENTRE => appendChr #"U"
| UP => appendChr #"U"
| RIGHT => appendChr #"U"
| DOWN => appendChr #"U"
| LEFT => appendChr #"U"
| L2 => appendChr #"U"
| R2 => #shiftChr state := true
else
case axisToDir (x, y, l2, r2) of
CENTRE => appendChr #"d"
| UP => appendChr #"h"
| RIGHT => appendChr #"l"
| DOWN => appendChr #"p"
| LEFT => appendChr #"t"
| L2 => appendChr #"x"
| R2 => #shiftChr state := true
end
in
fun handleIfJoystickIsPresent () =
let
open InputMsg
(* query possible events for information *)
val xAxis = Input.getLeftJoystickXAxisState ()
val yAxis = Input.getLeftJoystickYAxisState ()
val r2 = (Input.getR2State () + 1.0) / 2.0
val l2 = (Input.getL2State () + 1.0) / 2.0
val crossPressed = Input.isCrossButtonPressed ()
val circlePressed = Input.isCircleButtonPressed ()
val squarePressed = Input.isSquareButtonPressed ()
val trianglePressed = Input.isTriangleButtonPressed ()
val r1Pressed = Input.isR1ButtonPressed ()
val l1Pressed = Input.isL1ButtonPressed ()
(* handle button events *)
val () =
if crossPressed = 0 then #crossPressed state := false
else if !(#crossPressed state) then ()
else handleCrossPressed (xAxis, yAxis, l2, r2)
val () =
if circlePressed = 0 then #circlePressed state := false
else if !(#circlePressed state) then ()
else handleCirclePressed (xAxis, yAxis, l2, r2)
val () =
if squarePressed = 0 then #squarePressed state := false
else handleSquarePressed (xAxis, yAxis, l2, r2)
val () =
if trianglePressed = 0 then #trianglePressed state := false
else if !(#trianglePressed state) then ()
else handleTrianglePressed (xAxis, yAxis, l2, r2)
val () =
if r1Pressed = 0 then
#r1Pressed state := false
else if !(#r1Pressed state) then
()
else
let val () = InputMailbox.append (InputMsg.CHAR_EVENT #" ")
in #r1Pressed state := true
end
val () =
if l1Pressed = 0 then
#l1Pressed state := false
else if !(#l1Pressed state) then
()
else
let val () = InputMailbox.append InputMsg.KEY_BACKSPACE
in #l1Pressed state := true
end
val () =
(* if l2 and r2 are both pressed, then send escape event *)
if l2 > 0.3 andalso r2 > 0.3 then
InputMailbox.append InputMsg.KEY_ESC
else
()
in
()
end
end
fun query () =
if getGamepadState () then handleIfJoystickIsPresent ()
else (* nothing to do if no gamepad is present *) ()
end end