flan/examples/core-input-gamepad.flan
Joseph Ferano afec482722 Ten raylib examples, and what they could not say
The first ten of raylib's core list, ported. Seven new bindings and the
named colour palette; nothing else was added, because a binding called
by nothing is the same as not having bound it.

The gaps they found are the point. No number reaches draw-text: i64->bytes
answers [u8], draw-text wants a string, and nothing bridges — five of the
ten wanted TextFormat and got a glyph table instead. And an enum parameter
cannot be driven by a loop variable: the index is an i32, the parameter is
an enum, neither converts, and a second declare-c with an i32 face is
refused because one C function gets one binding. Two correct rules that
compose into a wall.

None of the gaps expected blocked anything: no generics, no allocator, no
Vec, no escaping closure, no block-scoped defer. These are input-and-draw
programs over fixed-size state, which is the shape the language has.
2026-09-12 05:06:01 +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. That cannot go through the binding,
;; twice over:
;;
;; expected rl/GamepadAxis, found i32
;;
;; because an integer does not convert to an enum and a keyword can only name
;; one member; and a second declare-c of the same C function with an i32
;; parameter is refused as well —
;;
;; rl/get-gamepad-axis-movement and rl/get-gamepad-axis-movement-by-index
;; both bind the C function GetGamepadAxisMovement — one declare-c per C
;; function, and another Flan name for it is a defn
;;
;; — and a defn wrapper cannot change a parameter's type. So the index is
;; turned back into a member here, by hand, which is the only shape left. It
;; covers the six axes raylib names; a pad reporting more reads 0.0 for the
;; extras, and the count is clamped below so they are not drawn at all.
(defn axis-at [pad i32 index i32] f32
(cond
(= index 0) (rl/get-gamepad-axis-movement pad :left-x)
(= index 1) (rl/get-gamepad-axis-movement pad :left-y)
(= index 2) (rl/get-gamepad-axis-movement pad :right-x)
(= index 3) (rl/get-gamepad-axis-movement pad :right-y)
(= index 4) (rl/get-gamepad-axis-movement pad :left-trigger)
(= index 5) (rl/get-gamepad-axis-movement pad :right-trigger)
:else 0.0))
(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))))