flan/examples/core-input-gamepad.flan
Joseph Ferano 26c53e0a19 Every defn in the tree states its return type, and Unit is written ()
The mechanical half, ahead of the parser change that needs it. tools/unit-return.py
fills the empty slot with () and rewrites Unit as () wherever a type is spelled --
(Fn [i32] Unit), (Map i32 Unit), a return type written out.

Deciding whether a defn already had a return type is the whole difficulty, and
the script does it the way parse.ml did: is_type_form is transcribed rather than
improved, because being identical to the parser it replaces is what makes the
sweep meaning-preserving. It is re-runnable, so the lanes that branched before
this can have the same pass at merge:

    python3 tools/unit-return.py .
    python3 tools/unit-return.py --in-strings test/test_flan.ml test/test_acceptance.ml \
        test/test_session.ml emacs/test-flan-dev.el emacs/test-flan-mode.el
    python3 tools/unit-return.py --raw-ml lib/prelude.ml
    python3 tools/unit-return.py --in-html web/index.html

-v logs every defn it saw and what it decided, which is how a sweep of 440 sites
gets reviewed at all. Embedded modes pool a file's type declarations across all
its fragments, because a snippet split across concatenation -- decls ^ "(defn f
[s [u8]] Cursor ...)" -- cannot see the names the other half declared; pooled
names count only in bare-symbol position, for the same reason the prelude's do.
A fragment that cuts off mid-form is skipped rather than guessed at. Five sites
in test_flan.ml still needed a hand, and they are in this commit.

Two things ride along because the sweep needs them: parse.ml reads a lone () as
the return type of a function with no body, which was not a shape the old
optional slot could produce; and the map refusals name () rather than Unit, since
that is now the spelling a caller wrote.
2026-09-12 23:06:40 +07:00

281 lines
13 KiB
Plaintext

;;;; raylib [core] example - input gamepad
;;;;
;;;; examples/core/core_input_gamepad.c. Needed set-config-flags (and the
;;;; FLAG_ constants), both now bound.
;;;;
;;;; Three deliberate differences from the C, all of them things the port
;;;; could not or should not follow.
;;;;
;;;; **The pad artwork is gone, and with it the xbox/PS branches.** The C draws
;;;; `resources/ps3.png` or `resources/xbox.png` and picks between them by
;;;; matching `GetGamepadName` against four substrings. Neither image is in
;;;; this repository, and importing two binaries for one example is worse than
;;;; the alternative: the C has a third branch, for a pad it does not
;;;; recognise, which draws the whole thing out of rounded rectangles and
;;;; circles and needs no asset at all. That branch is what this ports, in
;;;; full. It is also the branch a Linux desktop usually takes anyway.
;;;;
;;;; **GetGamepadName is not bound, and cannot be.** It is what the C matches
;;;; on, and declare-c refuses it by name:
;;;;
;;;; the return type of get-gamepad-name is a string, and a string only
;;;; crosses as a parameter — a C function that *returns* one returns
;;;; something Flan has no owner for
;;;;
;;;; which is correct: raylib hands back a pointer into its own static storage
;;;; and Flan has nothing that owns a borrowed C string. So the pad's name is
;;;; not on screen and the branch that used it is not here.
;;;;
;;;; **The VIBRATE button is drawn and inert.** SetGamepadVibration is the one
;;;; call in the C this refuses to bind, and vendor/raylib/raylib.flan already
;;;; carries the paragraph saying why: its arity differs between raylib
;;;; versions with no 5.5 header here to settle it, so a guess is a corrupted
;;;; stack frame rather than a link error — and the symbol in libraylib.so.550
;;;; disassembles to a single TraceLog call and a jump. Binding it would be
;;;; binding a warning. The button is drawn because taking it out would hide
;;;; the finding; it is labelled so.
;;;;
;;;; With no pad attached this shows "GP0: NOT DETECTED" and nothing else,
;;;; which is the C's behaviour minus the greyed-out artwork.
(import rl "vendor:raylib")
(import d "digits.flan")
(defconst screen-width 800)
(defconst screen-height 450)
(defconst stick-deadzone f32 0.1)
(defconst trigger-deadzone f32 -0.9)
(defvar gamepad i32)
;; The C's deadzone test, which is a band around zero and not a clamp: inside
;; it the axis reads exactly 0, outside it the raw value passes through
;; unscaled. That is a step at the edge of the band and the C has it too.
(defn deadzone [v f32 limit f32] f32
(if (and (> v (- 0.0 limit)) (< v limit)) 0.0 v))
;; The triggers rest at -1 rather than 0, which raylib does not normalise —
;; see the note on GamepadAxis in vendor/raylib/raylib.flan. So their deadzone
;; is a floor near the resting end and not a band around the middle.
(defn trigger-deadzoned [v f32] f32
(if (< v trigger-deadzone) -1.0 v))
;; The C's read-out loop is `for (i = 0; i < GetGamepadAxisCount(gamepad); i++)
;; DrawText(TextFormat("AXIS %i: %.02f", i, GetGamepadAxisMovement(gamepad, i)))`
;; — an axis selected by a loop variable, which is what this is: an integer
;; converts to an enum when the conversion is written by name.
;;
;; It used to be a six-armed cond over the members, because an enum is its own
;; type in the checker and a bare i32 does not fit one:
;;
;; expected rl/GamepadAxis, found i32
;;
;; That rule is unchanged and should be. It is what makes a misspelled
;; `:lft-x` an error *here*, at the call site, rather than a wrong axis read
;; later — which is the whole reason an enum is a type at all rather than a
;; pile of i32 constants. What changed is that the conversion can be said out
;; loud: the rule was never "an integer is dangerous", it was "an integer must
;; not arrive silently", and (rl/GamepadAxis index) is not silent.
;;
;; (The other escape stays closed, and should: a second declare-c of
;; GetGamepadAxisMovement with an i32 face is still refused — one declare-c
;; per C function — and a defn wrapper still cannot retype a parameter. It is
;; no longer needed.)
;;
;; An index raylib does not name converts too, rather than being refused, and
;; raylib bounds-checks its own axis and answers 0.0. So the (min 6 ...) clamp
;; at the call site is what keeps the read-out to the six axes raylib names —
;; load-bearing now rather than cosmetic, since it replaced the :else arm.
(defn axis-at [pad i32 index i32] f32
(rl/get-gamepad-axis-movement pad (rl/GamepadAxis index)))
(defn draw-pad-background [] ()
(rl/draw-rectangle-rounded
(rl/Rectangle {.x 175.0 .y 110.0 .width 460.0 .height 220.0})
0.3 16 rl/darkgray)
;; The three middle buttons and the four face buttons, as outlines. The
;; filled overlays go on top only while the button is down.
(rl/draw-circle 365 170 12.0 rl/raywhite)
(rl/draw-circle 405 170 12.0 rl/raywhite)
(rl/draw-circle 445 170 12.0 rl/raywhite)
(rl/draw-circle 516 191 17.0 rl/raywhite)
(rl/draw-circle 551 227 17.0 rl/raywhite)
(rl/draw-circle 587 191 17.0 rl/raywhite)
(rl/draw-circle 551 155 17.0 rl/raywhite)
;; The d-pad cross: a light plate with a dark one inset, so a pressed
;; direction has something to show up against.
(rl/draw-rectangle 245 145 28 88 rl/raywhite)
(rl/draw-rectangle 215 174 88 29 rl/raywhite)
(rl/draw-rectangle 247 147 24 84 rl/black)
(rl/draw-rectangle 217 176 84 25 rl/black)
(rl/draw-rectangle-rounded
(rl/Rectangle {.x 215.0 .y 98.0 .width 100.0 .height 10.0})
0.5 16 rl/darkgray)
(rl/draw-rectangle-rounded
(rl/Rectangle {.x 495.0 .y 98.0 .width 100.0 .height 10.0})
0.5 16 rl/darkgray))
(defn draw-pad-buttons [] ()
(when (rl/gamepad-button-down? gamepad :middle-left)
(rl/draw-circle 365 170 10.0 rl/red))
(when (rl/gamepad-button-down? gamepad :middle)
(rl/draw-circle 405 170 10.0 rl/green))
(when (rl/gamepad-button-down? gamepad :middle-right)
(rl/draw-circle 445 170 10.0 rl/blue))
(when (rl/gamepad-button-down? gamepad :right-face-left)
(rl/draw-circle 516 191 15.0 rl/gold))
(when (rl/gamepad-button-down? gamepad :right-face-down)
(rl/draw-circle 551 227 15.0 rl/blue))
(when (rl/gamepad-button-down? gamepad :right-face-right)
(rl/draw-circle 587 191 15.0 rl/green))
(when (rl/gamepad-button-down? gamepad :right-face-up)
(rl/draw-circle 551 155 15.0 rl/red))
(when (rl/gamepad-button-down? gamepad :left-face-up)
(rl/draw-rectangle 247 147 24 29 rl/red))
(when (rl/gamepad-button-down? gamepad :left-face-down)
(rl/draw-rectangle 247 201 24 30 rl/red))
(when (rl/gamepad-button-down? gamepad :left-face-left)
(rl/draw-rectangle 217 176 30 25 rl/red))
(when (rl/gamepad-button-down? gamepad :left-face-right)
(rl/draw-rectangle 271 176 30 25 rl/red))
(when (rl/gamepad-button-down? gamepad :left-trigger-1)
(rl/draw-rectangle-rounded
(rl/Rectangle {.x 215.0 .y 98.0 .width 100.0 .height 10.0})
0.5 16 rl/red))
(when (rl/gamepad-button-down? gamepad :right-trigger-1)
(rl/draw-rectangle-rounded
(rl/Rectangle {.x 495.0 .y 98.0 .width 100.0 .height 10.0})
0.5 16 rl/red)))
(defn draw-stick [cx i32 cy i32 ax f32 ay f32 thumb-down bool] ()
(rl/draw-circle cx cy 40.0 rl/black)
(rl/draw-circle cx cy 35.0 rl/lightgray)
(rl/draw-circle (+ cx (i32 (* ax 20.0))) (+ cy (i32 (* ay 20.0))) 25.0
(if thumb-down rl/red rl/black)))
(defn main [] ()
;; Before init-window, and it has to be: raylib reads the flags while it is
;; creating the context, so the same call afterwards is accepted, logged, and
;; has no effect on the window that already exists.
(rl/set-config-flags rl/flag-msaa-4x-hint)
(rl/init-window screen-width screen-height
"raylib [core] example - input gamepad")
(defer (rl/close-window))
(rl/set-target-fps 60)
(until (rl/window-should-close?)
;; Update
(when (and (rl/key-pressed? :left) (> gamepad 0))
(set gamepad (- gamepad 1)))
(when (rl/key-pressed? :right) (set gamepad (+ gamepad 1)))
(let [axis-count (min 6 (rl/get-gamepad-axis-count gamepad))
vibrate-rect (rl/Rectangle {.x 10.0
.y (+ 90.0 (* 20.0 (f32 axis-count)))
.width 75.0 .height 24.0})]
;; Draw
(rl/begin-drawing)
(rl/clear-background rl/raywhite)
(if (rl/gamepad-available? gamepad)
(do
;; The C draws `TextFormat("GP%d: %s", gamepad, GetGamepadName(...))`.
;; The name is unbindable — see the header — so this is the index and
;; the word the C would have followed it with.
(rl/draw-text "GP" 10 10 10 rl/black)
(let [x (+ 10 (rl/measure-text "GP" 10))]
(rl/draw-text ": CONNECTED (name unbindable)"
(+ x (d/draw-int gamepad x 10 10 rl/black))
10 10 rl/black))
(let [lx (deadzone (rl/get-gamepad-axis-movement gamepad :left-x)
stick-deadzone)
ly (deadzone (rl/get-gamepad-axis-movement gamepad :left-y)
stick-deadzone)
rx (deadzone (rl/get-gamepad-axis-movement gamepad :right-x)
stick-deadzone)
ry (deadzone (rl/get-gamepad-axis-movement gamepad :right-y)
stick-deadzone)
lt (trigger-deadzoned
(rl/get-gamepad-axis-movement gamepad :left-trigger))
rt (trigger-deadzoned
(rl/get-gamepad-axis-movement gamepad :right-trigger))]
(draw-pad-background)
(draw-pad-buttons)
(draw-stick 345 260 lx ly
(rl/gamepad-button-down? gamepad :left-thumb))
(draw-stick 465 260 rx ry
(rl/gamepad-button-down? gamepad :right-thumb))
;; The triggers as bars filling upward from a grey track. The +1
;; and the halving turn raylib's [-1, 1] into [0, 1].
(rl/draw-rectangle 151 110 15 70 rl/gray)
(rl/draw-rectangle 644 110 15 70 rl/gray)
(rl/draw-rectangle 151 110 15 (i32 (* (/ (+ 1.0 lt) 2.0) 70.0))
rl/red)
(rl/draw-rectangle 644 110 15 (i32 (* (/ (+ 1.0 rt) 2.0) 70.0))
rl/red))
(rl/draw-text "DETECTED AXIS [" 10 50 10 rl/maroon)
(let [x (+ 10 (rl/measure-text "DETECTED AXIS [" 10))]
(rl/draw-text "]:" (+ x (d/draw-int axis-count x 50 10 rl/maroon))
50 10 rl/maroon))
(dotimes [i axis-count]
(rl/draw-text "AXIS " 20 (+ 70 (* 20 i)) 10 rl/darkgray)
(let [x (+ 20 (rl/measure-text "AXIS " 10))
y (+ 70 (* 20 i))]
(set x (+ x (d/draw-int i x y 10 rl/darkgray)))
(rl/draw-text ": " x y 10 rl/darkgray)
(set x (+ x (rl/measure-text ": " 10)))
;; The raw reading, not the deadzoned one — the C shows the raw
;; value here too, which is what makes the deadzone visible as a
;; difference between this row and the stick.
(d/draw-f32 (axis-at gamepad i) 2 x y 10
rl/darkgray)))
;; Drawn and inert. See the header: SetGamepadVibration is a stub in
;; this raylib and is not bound.
(rl/draw-rectangle-rec vibrate-rect rl/skyblue)
(rl/draw-text "VIBRATE" (+ (i32 (.x vibrate-rect)) 14)
(+ (i32 (.y vibrate-rect)) 1) 10 rl/darkgray)
(rl/draw-text "(not bound: raylib stub)" 95
(+ (i32 (.y vibrate-rect)) 7) 10 rl/gray)
;; -1 when nothing is pressed, which is why the binding answers an
;; i32 and not a GamepadButton.
;; draw-int answers the width it drew, so a branch that ends in one
;; has type i32 while its sibling has type Unit and the `if` will not
;; typecheck — "expected i32, found Unit". Both arms end in a
;; draw-text here, which is the tidy way out; where that is awkward a
;; trailing `(do)` is the other.
(let [b (rl/get-gamepad-button-pressed)]
(if (>= b 0)
(do (rl/draw-text "DETECTED BUTTON: " 10 430 10 rl/red)
(d/draw-int b (+ 10 (rl/measure-text "DETECTED BUTTON: " 10))
430 10 rl/red)
(do))
(rl/draw-text "DETECTED BUTTON: NONE" 10 430 10 rl/gray))))
(do
(rl/draw-text "GP" 10 10 10 rl/gray)
(let [x (+ 10 (rl/measure-text "GP" 10))]
(rl/draw-text ": NOT DETECTED"
(+ x (d/draw-int gamepad x 10 10 rl/gray))
10 10 rl/gray))
(rl/draw-text "left/right arrows select another pad" 10 30 10
rl/lightgray)))
(rl/end-drawing))))