diff --git a/NEXT.md b/NEXT.md index b7beb71..2d61380 100644 --- a/NEXT.md +++ b/NEXT.md @@ -302,7 +302,9 @@ that calls an existing binding is unaffected. 34 51 68`, four separate bytes — a `Color` is *not* the little-endian reading of the packed integer, so an identity would have passed a weaker test. That case is in the acceptance table, skipped if `libraylib` is not installed. -The bindings are 164 calls across thirteen structs: window, keyboard and mouse; drawing (rectangles, circles, lines, triangles, rings, ellipses, text); the eleven `collision-*` predicates; textures; the Image family; `Camera2D`; `RenderTexture2D`; the whole audio surface (device, `Wave`, `Sound`, `Music`); fonts and glyphs; and gamepads, touch and gestures — plus the `Key`, `MouseButton`, `TraceLogLevel`, `GamepadButton`, `GamepadAxis` and `Gesture` enums. Adding one is a single `declare-c` line; there is no C to write. +The bindings are 171 calls across thirteen structs: window, keyboard and mouse; drawing (rectangles, circles, lines, triangles, rings, ellipses, text); the eleven `collision-*` predicates; textures; the Image family; `Camera2D`; `RenderTexture2D`; the whole audio surface (device, `Wave`, `Sound`, `Music`); fonts and glyphs; and gamepads, touch and gestures — plus the `Key`, `MouseButton`, `TraceLogLevel`, `GamepadButton`, `GamepadAxis` and `Gesture` enums, raylib's own named colour palette, and the `FLAG_` window hints. Adding one is a single `declare-c` line; there is no C to write. + +Two things the ported examples in `examples/` wanted and could not have, both refused for reasons that are right. `GetGamepadName` returns a `char *` into raylib's static storage: *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*. And an enum parameter cannot be indexed — `GetGamepadAxisMovement` takes a `GamepadAxis`, a loop variable is an `i32`, *expected rl/GamepadAxis, found i32*, and a second `declare-c` of the same symbol with an `i32` face is refused too: *one declare-c per C function, and another Flan name for it is a defn* — which cannot help, because a wrapper renames and does not retype. The caller spells the loop as a `cond` over the members it knows. The texture calls are the first ones with no headless test, because loading one needs a GL context. What the acceptance case does instead is pin the two new struct layouts using the only things raylib computes from those fields without a diff --git a/examples/core-basic-window.flan b/examples/core-basic-window.flan new file mode 100644 index 0000000..6afbeec --- /dev/null +++ b/examples/core-basic-window.flan @@ -0,0 +1,32 @@ +;;;; raylib [core] example - basic window +;;;; +;;;; examples/core/core_basic_window.c, line for line. Nothing here needed +;;;; anything the language did not already have, which is the point of doing it +;;;; first: if this one does not run, none of the others will either. +;;;; +;;;; The one structural difference from the C is the loop: the C writes +;;;; `while (!WindowShouldClose())` and Flan has `until`, which is the same +;;;; thing without the negation. sand.flan does likewise. +;;;; +;;;; `defer` closes the window. It is function-scoped — it runs when `main` +;;;; returns and not at the end of any inner block — which is exactly what the +;;;; C's trailing CloseWindow() means here. + +(import rl "vendor:raylib") + +(defconst screen-width 800) +(defconst screen-height 450) + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - basic window") + (defer (rl/close-window)) + + (rl/set-target-fps 60) + + (until (rl/window-should-close?) + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + (rl/draw-text "Congrats! You created your first window!" 190 200 20 + rl/lightgray) + (rl/end-drawing))) diff --git a/examples/core-delta-time.flan b/examples/core-delta-time.flan new file mode 100644 index 0000000..c0a3706 --- /dev/null +++ b/examples/core-delta-time.flan @@ -0,0 +1,115 @@ +;;;; raylib [core] example - delta time +;;;; +;;;; examples/core/core_delta_time.c. Needed get-mouse-wheel-move and get-fps, +;;;; both now bound — draw-fps was already there, but it puts the number on the +;;;; screen itself and this example wants to compare it against the target it +;;;; set, so it needs the number back. +;;;; +;;;; Three TextFormats in the C, all drawn here by examples/digits.flan. +;;;; +;;;; One faithfulness note that is a bug in the C and is kept anyway: it draws +;;;; `TextFormat("Frame time: %02.02f ms", GetFrameTime())` — GetFrameTime is +;;;; in SECONDS, so at 60 fps the C's own screen reads "0.02 ms" for what is +;;;; really 16.7 ms. Porting it as-is means the two programs show the same +;;;; thing, which is what a port is for; the milliseconds are drawn beside it +;;;; here rather than instead of it, so the file is not quietly asserting that +;;;; the original was right. +;;;; +;;;; The two circle positions are `defvar`s for the usual reason: they are +;;;; state between frames and a `let` inside the loop would reset them. + +(import rl "vendor:raylib") +(import d "digits.flan") + +(defconst screen-width 800) +(defconst screen-height 450) + +(defconst speed f32 10.0) +(defconst circle-radius f32 32.0) + +(defvar delta-circle rl/Vector2) +(defvar frame-circle rl/Vector2) +(defvar current-fps i32) + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - delta time") + (defer (rl/close-window)) + + (set current-fps 60) + (set delta-circle (rl/Vector2 {:x 0.0 :y (/ (f32 screen-height) 3.0)})) + (set frame-circle (rl/Vector2 {:x 0.0 + :y (* (f32 screen-height) (/ 2.0 3.0))})) + + (rl/set-target-fps current-fps) + + (until (rl/window-should-close?) + ;; Update + (let [wheel (rl/get-mouse-wheel-move)] + (unless (= wheel 0.0) + (set current-fps (+ current-fps (i32 wheel))) + (when (< current-fps 0) (set current-fps 0)) + (rl/set-target-fps current-fps))) + + ;; The whole point of the example: one circle is scaled by the frame time + ;; and one is not, so lowering the target with the wheel makes the second + ;; one crawl while the first keeps its speed. + (set (.x delta-circle) + (+ (.x delta-circle) (* (rl/get-frame-time) (* 6.0 speed)))) + (set (.x frame-circle) (+ (.x frame-circle) (* 0.1 speed))) + + (when (> (.x delta-circle) (f32 screen-width)) (set (.x delta-circle) 0.0)) + (when (> (.x frame-circle) (f32 screen-width)) (set (.x frame-circle) 0.0)) + + (when (rl/key-pressed? :r) + (set (.x delta-circle) 0.0) + (set (.x frame-circle) 0.0)) + + ;; Draw + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + + (rl/draw-circle-v delta-circle circle-radius rl/red) + (rl/draw-circle-v frame-circle circle-radius rl/blue) + + ;; "FPS: unlimited (%i)" when the target is 0, "FPS: %i (target: %i)" + ;; otherwise — the C's own branch, reassembled out of literals and + ;; draw-int. `x` walks along the line as each piece is drawn, which is what + ;; the returned width from draw-int is for. + (if (<= current-fps 0) + (let [x 10] + (rl/draw-text "FPS: unlimited (" x 10 20 rl/darkgray) + (set x (+ x (rl/measure-text "FPS: unlimited (" 20))) + (set x (+ x (d/draw-int (rl/get-fps) x 10 20 rl/darkgray))) + (rl/draw-text ")" x 10 20 rl/darkgray)) + (let [x 10] + (rl/draw-text "FPS: " x 10 20 rl/darkgray) + (set x (+ x (rl/measure-text "FPS: " 20))) + (set x (+ x (d/draw-int (rl/get-fps) x 10 20 rl/darkgray))) + (rl/draw-text " (target: " x 10 20 rl/darkgray) + (set x (+ x (rl/measure-text " (target: " 20))) + (set x (+ x (d/draw-int current-fps x 10 20 rl/darkgray))) + (rl/draw-text ")" x 10 20 rl/darkgray))) + + ;; The C's line, kept exactly — seconds, labelled ms. + (let [x 10] + (rl/draw-text "Frame time: " x 30 20 rl/darkgray) + (set x (+ x (rl/measure-text "Frame time: " 20))) + (set x (+ x (d/draw-f32 (rl/get-frame-time) 2 x 30 20 rl/darkgray))) + (rl/draw-text " ms" x 30 20 rl/darkgray)) + ;; And the same number in the unit the label claims, so the file does not + ;; have to be read to notice. + (let [x 10] + (rl/draw-text "(really " x 50 20 rl/lightgray) + (set x (+ x (rl/measure-text "(really " 20))) + (set x (+ x (d/draw-f32 (* (rl/get-frame-time) 1000.0) 2 x 50 20 + rl/lightgray))) + (rl/draw-text " ms)" x 50 20 rl/lightgray)) + + (rl/draw-text "Use the scroll wheel to change the fps limit, r to reset" + 10 70 20 rl/darkgray) + + (rl/draw-text "FUNC: x += GetFrameTime()*speed" 10 110 20 rl/red) + (rl/draw-text "FUNC: x += speed" 10 260 20 rl/blue) + + (rl/end-drawing))) diff --git a/examples/core-input-gamepad.flan b/examples/core-input-gamepad.flan new file mode 100644 index 0000000..6b76b25 --- /dev/null +++ b/examples/core-input-gamepad.flan @@ -0,0 +1,280 @@ +;;;; 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)))) diff --git a/examples/core-input-gestures-testbed.flan b/examples/core-input-gestures-testbed.flan new file mode 100644 index 0000000..5773586 --- /dev/null +++ b/examples/core-input-gestures-testbed.flan @@ -0,0 +1,345 @@ +;;;; raylib [core] example - input gestures testbed +;;;; +;;;; examples/core/core_input_gestures_testbed.c, the four-star one and the +;;;; hardest of the ten. It runs, and it is the example that pushed hardest on +;;;; the language. Four things it needed, in descending order of how much they +;;;; cost. +;;;; +;;;; **An enum does not convert to an integer.** The C treats the gesture as +;;;; the raw bitfield it is and compares it with `<` and `>`: +;;;; `currentGesture > 255` picks out the two pinches, `> 15` the four swipes, +;;;; `!= 4` excludes hold, `< 3` admits tap and double-tap. `get-gesture- +;;;; detected` answers an `rl/Gesture`, and +;;;; +;;;; i32 converts a number, found rl/Gesture +;;;; +;;;; so none of those comparisons can be written. They are spelled out below as +;;;; named predicates over keyword equalities — `pinch?`, `swipe?`, `tapish?`. +;;;; That is arguably better source than the magic numbers were, and it is +;;;; strictly more checkable, but it is not a choice: it is the only shape +;;;; available, and a gesture raylib adds later would silently fall out of +;;;; `swipe?` where the C's `> 15` would have caught it. +;;;; +;;;; **No sin or cos.** The prelude has sqrt-f32 — one `declare` over libm, +;;;; with a comment explaining why it is not a builtin — and nothing else +;;;; transcendental. The protractor needs both, so they are two more `declare` +;;;; lines here, in the same shape. They link: libm is already on the line. +;;;; +;;;; **No string formatting**, as everywhere. The C prints the angle with +;;;; `TextFormat("%f", ...)`, finds the decimal point with `TextFindIndex` and +;;;; cuts two digits past it with `TextSubtext`. `draw-f32` in +;;;; examples/digits.flan does the whole thing in one call, and rounds rather +;;;; than truncating — which is the one place this screen differs from the C's +;;;; by a digit. +;;;; +;;;; **No local fixed arrays.** The C declares `char gestureLog[20][12]` and +;;;; `Vector2 touchPosition[32]` inside main. A `let` binding takes no type +;;;; annotation, so a fixed array can only be a top-level `defvar` or a literal +;;;; with every element written out — thirty-two Vector2s, here. They are +;;;; `defvar`s, which is what the C's storage amounts to anyway. +;;;; +;;;; The log itself came out simpler than the C's: the names are compile-time +;;;; literals, so a slot holds a `string` and there is no TextCopy and no +;;;; twelve-byte truncation. + +(import rl "vendor:raylib") +(import d "digits.flan") + +;; libm, as the prelude declares sqrtf. Not declare-c: these take a float and +;; answer a float in C's own convention with no struct anywhere, which is what +;; plain `declare` is for. +(declare sin-f32 [x f32] f32 "sinf") +(declare cos-f32 [x f32] f32 "cosf") + +(defconst screen-width 800) +(defconst screen-height 450) + +(defconst pi f32 3.14159265) + +(defconst gesture-log-size 20) +(defconst max-touch-count 32) + +(defvar gesture-log [gesture-log-size string]) +;; The C's inverted circular queue: the index counts DOWN and wraps at the top, +;; so the newest entry is always at gesture-log-index and the draw loop walks +;; forward from there. Starting at the size rather than at size-1 is the C's +;; too — the first write decrements before storing. +(defvar gesture-log-index i32) +(defvar previous-gesture rl/Gesture) +(defvar last-gesture rl/Gesture) +(defvar gesture-color rl/Color) +(defvar log-mode i32) +(defvar current-angle f32) +(defvar touch-positions [max-touch-count rl/Vector2]) + +;; ── The comparisons the C makes on the raw bitfield ───────────────── +;; +;; See the header: an rl/Gesture will not convert to an i32, so `> 255` and +;; friends are these instead. + +(defn pinch? [g rl/Gesture] bool ; the C's `> 255` + (or (= g :pinch-in) (= g :pinch-out))) + +(defn swipe? [g rl/Gesture] bool ; the C's `> 15` + (or (or (= g :swipe-right) (= g :swipe-left)) + (or (= g :swipe-up) (= g :swipe-down)))) + +(defn tapish? [g rl/Gesture] bool ; the C's `< 3` + (or (= g :tap) (= g :double-tap))) + +(defn gesture-name [g rl/Gesture] string + (cond + (= g :none) "None" + (= g :tap) "Tap" + (= g :double-tap) "Double Tap" + (= g :hold) "Hold" + (= g :drag) "Drag" + (= g :swipe-right) "Swipe Right" + (= g :swipe-left) "Swipe Left" + (= g :swipe-up) "Swipe Up" + (= g :swipe-down) "Swipe Down" + (= g :pinch-in) "Pinch In" + (= g :pinch-out) "Pinch Out" + :else "Unknown")) + +(defn gesture-color-of [g rl/Gesture] rl/Color + (cond + (= g :tap) rl/blue + (= g :double-tap) rl/skyblue + (= g :drag) rl/lime + (swipe? g) rl/red + (= g :pinch-in) rl/violet + (= g :pinch-out) rl/orange + :else rl/black)) + +;; ── The log ───────────────────────────────────────────────────────── + +;; The C's four log modes, as a truth table rather than as nested ifs: +;; 0 shows repeated events +;; 1 hides repeated events +;; 2 shows repeated events but hides hold +;; 3 hides repeated events and hides hold +(defn should-log? [g rl/Gesture] bool + (cond + (= g :none) false + (= log-mode 3) (or (and (not (= g :hold)) (not (= g previous-gesture))) + (tapish? g)) + (= log-mode 2) (not (= g :hold)) + (= log-mode 1) (not (= g previous-gesture)) + :else true)) + +(defn push-log [g rl/Gesture] + (set previous-gesture g) + (set gesture-color (gesture-color-of g)) + (when (<= gesture-log-index 0) (set gesture-log-index gesture-log-size)) + (set gesture-log-index (- gesture-log-index 1)) + (set (at gesture-log gesture-log-index) (gesture-name g))) + +;; ── Drawing ───────────────────────────────────────────────────────── + +(defconst last-x 165) +(defconst last-y 130) +(defconst prot-x f32 266.0) +(defconst prot-y f32 315.0) +(defconst angle-length f32 90.0) + +(defn swipe-box [gx i32 gy i32 which rl/Gesture] + (rl/draw-rectangle gx gy 20 20 + (if (= last-gesture which) rl/red rl/lightgray))) + +(defn draw-last-gesture [touch-count i32] + (rl/draw-text "Last gesture" (+ last-x 33) (- last-y 47) 20 rl/black) + (rl/draw-text "Swipe Tap Pinch Touch" (+ last-x 17) + (- last-y 18) 10 rl/black) + + (swipe-box (+ last-x 20) last-y :swipe-up) + (swipe-box last-x (+ last-y 20) :swipe-left) + (swipe-box (+ last-x 40) (+ last-y 20) :swipe-right) + (swipe-box (+ last-x 20) (+ last-y 40) :swipe-down) + + (rl/draw-circle (+ last-x 80) (+ last-y 16) 10.0 + (if (= last-gesture :tap) rl/blue rl/lightgray)) + ;; segments 0 lets raylib pick the count from the radius. + (rl/draw-ring (rl/Vector2 {:x (f32 (+ last-x 103)) :y (f32 (+ last-y 16))}) + 6.0 11.0 0.0 360.0 0 + (if (= last-gesture :drag) rl/lime rl/lightgray)) + (rl/draw-circle (+ last-x 80) (+ last-y 43) 10.0 + (if (= last-gesture :double-tap) rl/skyblue rl/lightgray)) + (rl/draw-circle (+ last-x 103) (+ last-y 43) 10.0 + (if (= last-gesture :double-tap) rl/skyblue rl/lightgray)) + + ;; The two pairs of arrowheads, pointing outward for pinch-out and inward + ;; for pinch-in. Counter-clockwise winding, or raylib culls them. + (let [out-c (if (= last-gesture :pinch-out) rl/orange rl/lightgray) + in-c (if (= last-gesture :pinch-in) rl/violet rl/lightgray)] + (rl/draw-triangle (rl/Vector2 {:x (f32 (+ last-x 122)) :y (f32 (+ last-y 16))}) + (rl/Vector2 {:x (f32 (+ last-x 137)) :y (f32 (+ last-y 26))}) + (rl/Vector2 {:x (f32 (+ last-x 137)) :y (f32 (+ last-y 6))}) + out-c) + (rl/draw-triangle (rl/Vector2 {:x (f32 (+ last-x 147)) :y (f32 (+ last-y 6))}) + (rl/Vector2 {:x (f32 (+ last-x 147)) :y (f32 (+ last-y 26))}) + (rl/Vector2 {:x (f32 (+ last-x 162)) :y (f32 (+ last-y 16))}) + out-c) + (rl/draw-triangle (rl/Vector2 {:x (f32 (+ last-x 125)) :y (f32 (+ last-y 33))}) + (rl/Vector2 {:x (f32 (+ last-x 125)) :y (f32 (+ last-y 53))}) + (rl/Vector2 {:x (f32 (+ last-x 140)) :y (f32 (+ last-y 43))}) + in-c) + (rl/draw-triangle (rl/Vector2 {:x (f32 (+ last-x 144)) :y (f32 (+ last-y 43))}) + (rl/Vector2 {:x (f32 (+ last-x 159)) :y (f32 (+ last-y 53))}) + (rl/Vector2 {:x (f32 (+ last-x 159)) :y (f32 (+ last-y 33))}) + in-c)) + + ;; Four pips, one per simultaneous touch raylib reports. + (dotimes [i 4] + (rl/draw-circle (+ last-x 180) (+ (+ last-y 7) (* i 15)) 5.0 + (if (<= touch-count i) rl/lightgray gesture-color)))) + +(defn draw-log [] + (rl/draw-text "Log" 10 10 20 rl/black) + ;; Forward from the newest, wrapping — the inverted queue read the right way + ;; round. + (dotimes [i gesture-log-size] + (let [ii (% (+ gesture-log-index i) gesture-log-size)] + (rl/draw-text (at gesture-log ii) 10 (- (+ 10 410) (* i 20)) 20 + (if (= i 0) gesture-color rl/lightgray)))) + + ;; The two mode buttons. Maroon means the mode that button controls is on. + (let [b1 (rl/Rectangle {:x 53.0 :y 7.0 :width 48.0 :height 26.0}) + b2 (rl/Rectangle {:x 108.0 :y 7.0 :width 36.0 :height 26.0})] + (rl/draw-rectangle-rec b1 (if (or (= log-mode 1) (= log-mode 3)) + rl/maroon rl/gray)) + (rl/draw-text "Hide" 60 10 10 rl/white) + (rl/draw-text "Repeat" 60 20 10 rl/white) + (rl/draw-rectangle-rec b2 (if (or (= log-mode 2) (= log-mode 3)) + rl/maroon rl/gray)) + (rl/draw-text "Hide" 115 10 10 rl/white) + (rl/draw-text "Hold" 115 20 10 rl/white))) + +(defn draw-protractor [] + (rl/draw-text "Angle" (+ (i32 prot-x) 55) (+ (i32 prot-y) 76) 10 rl/black) + ;; The C's TextFormat/TextFindIndex/TextSubtext dance to get two decimals, + ;; in one call. It rounds where the C truncated, so the last digit can + ;; differ by one. + (d/draw-f32 current-angle 2 (+ (i32 prot-x) 55) (+ (i32 prot-y) 92) 20 + gesture-color) + + (rl/draw-circle-v (rl/Vector2 {:x prot-x :y prot-y}) 80.0 rl/white) + (rl/draw-line-ex (rl/Vector2 {:x (- prot-x 90.0) :y prot-y}) + (rl/Vector2 {:x (+ prot-x 90.0) :y prot-y}) 3.0 rl/lightgray) + (rl/draw-line-ex (rl/Vector2 {:x prot-x :y (- prot-y 90.0)}) + (rl/Vector2 {:x prot-x :y (+ prot-y 90.0)}) 3.0 rl/lightgray) + (rl/draw-line-ex (rl/Vector2 {:x (- prot-x 80.0) :y (- prot-y 45.0)}) + (rl/Vector2 {:x (+ prot-x 80.0) :y (+ prot-y 45.0)}) 3.0 + rl/green) + (rl/draw-line-ex (rl/Vector2 {:x (- prot-x 80.0) :y (+ prot-y 45.0)}) + (rl/Vector2 {:x (+ prot-x 80.0) :y (- prot-y 45.0)}) 3.0 + rl/green) + + (rl/draw-text "0" (+ (i32 prot-x) 96) (- (i32 prot-y) 9) 20 rl/black) + (rl/draw-text "30" (+ (i32 prot-x) 74) (- (i32 prot-y) 68) 20 rl/black) + (rl/draw-text "90" (- (i32 prot-x) 11) (- (i32 prot-y) 110) 20 rl/black) + (rl/draw-text "150" (- (i32 prot-x) 100) (- (i32 prot-y) 68) 20 rl/black) + (rl/draw-text "180" (- (i32 prot-x) 124) (- (i32 prot-y) 9) 20 rl/black) + (rl/draw-text "210" (- (i32 prot-x) 100) (+ (i32 prot-y) 50) 20 rl/black) + (rl/draw-text "270" (- (i32 prot-x) 18) (+ (i32 prot-y) 92) 20 rl/black) + (rl/draw-text "330" (+ (i32 prot-x) 72) (+ (i32 prot-y) 50) 20 rl/black) + + ;; The needle. The C's +90 puts 0 degrees on the right, and sin feeding x + ;; against cos feeding y is what rotates it the way the dial is labelled. + (unless (= current-angle 0.0) + (let [rad (/ (* (+ current-angle 90.0) pi) 180.0)] + (rl/draw-line-ex (rl/Vector2 {:x prot-x :y prot-y}) + (rl/Vector2 {:x (+ (* angle-length (sin-f32 rad)) prot-x) + :y (+ (* angle-length (cos-f32 rad)) prot-y)}) + 3.0 gesture-color)))) + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - input gestures testbed") + (defer (rl/close-window)) + + ;; A zeroed Color has an alpha of 0 and is invisible; the C initialises this + ;; to opaque black. Same for the log index, which starts at the SIZE. + (set gesture-color rl/black) + (set gesture-log-index gesture-log-size) + (set log-mode 1) + + (rl/set-target-fps 60) + + (let [b1 (rl/Rectangle {:x 53.0 :y 7.0 :width 48.0 :height 26.0}) + b2 (rl/Rectangle {:x 108.0 :y 7.0 :width 36.0 :height 26.0})] + (until (rl/window-should-close?) + ;; Update + (let [g (rl/get-gesture-detected) + touch-count (min max-touch-count (rl/get-touch-point-count))] + + ;; The C filters hold out of the "last gesture" display, because hold + ;; repeats every frame and would drown everything else. + (when (and (and (not (= g :none)) (not (= g :hold))) + (not (= g previous-gesture))) + (set last-gesture g)) + + ;; The two mode buttons toggle one bit each of log-mode. + (when (rl/mouse-button-released? :left) + (let [m (rl/get-mouse-position)] + (when (rl/collision-point-rec? m b1) + (set log-mode (cond (= log-mode 3) 2 + (= log-mode 2) 3 + (= log-mode 1) 0 + :else 1))) + (when (rl/collision-point-rec? m b2) + (set log-mode (cond (= log-mode 3) 1 + (= log-mode 2) 0 + (= log-mode 1) 3 + :else 2))))) + + (when (should-log? g) (push-log g)) + + ;; The protractor reads the pinch angle for a pinch, the drag angle for + ;; a swipe, and sits at 0 for everything else. These are the C's + ;; `> 255` / `> 15` / `> 0` — see the header for why they are spelled + ;; this way. + (cond + (pinch? g) (set current-angle (rl/get-gesture-pinch-angle)) + (swipe? g) (set current-angle (rl/get-gesture-drag-angle)) + (not (= g :none)) (set current-angle 0.0) + :else (do)) + + (dotimes [i touch-count] + (set (at touch-positions i) (rl/get-touch-position i))) + + ;; Draw + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + + (rl/draw-text "*" 165 12 10 rl/black) + (rl/draw-text "Example optimized for Web/HTML5\non Smartphones with Touch Screen." + 175 12 10 rl/black) + (rl/draw-text "*" 165 42 10 rl/black) + (rl/draw-text "While running on Desktop Web Browsers,\ninspect and turn on Touch Emulation." + 175 42 10 rl/black) + + (draw-last-gesture touch-count) + (draw-log) + (draw-protractor) + + ;; The pointer itself: every live touch, or the mouse when there is no + ;; touchscreen. The halo is the gesture colour faded, which is what + ;; `fade` was bound for. + (unless (= g :none) + (if (> touch-count 0) + (do + (dotimes [i touch-count] + (let [p (at touch-positions i)] + (rl/draw-circle-v p 50.0 (rl/fade gesture-color 0.5)) + (rl/draw-circle-v p 5.0 gesture-color))) + ;; Two fingers: the line between them, thinner while pinching + ;; out, which is the C's only use of the raw 512. + (when (= touch-count 2) + (rl/draw-line-ex (at touch-positions 0) (at touch-positions 1) + (if (= g :pinch-out) 8.0 12.0) gesture-color))) + (let [m (rl/get-mouse-position)] + (rl/draw-circle-v m 35.0 (rl/fade gesture-color 0.5)) + (rl/draw-circle-v m 5.0 gesture-color)))) + + (rl/end-drawing))))) diff --git a/examples/core-input-gestures.flan b/examples/core-input-gestures.flan new file mode 100644 index 0000000..364c9e5 --- /dev/null +++ b/examples/core-input-gestures.flan @@ -0,0 +1,110 @@ +;;;; raylib [core] example - input gestures +;;;; +;;;; examples/core/core_input_gestures.c. Needed `fade`, now bound. +;;;; +;;;; The C keeps `char gestureStrings[20][32]` and TextCopy's a name into the +;;;; next slot. Flan has no mutable character buffer and no way to copy into +;;;; one, but it does not need either: the names are compile-time literals, so +;;;; the log is a `[20 string]` and a slot holds the literal itself. That is +;;;; strictly better than the C — no truncation at 32 bytes, no copy — and it +;;;; is only possible because every string that ever goes into the log is +;;;; known at compile time. A log of strings the program had BUILT could not be +;;;; written at all. +;;;; +;;;; The C's `switch (currentGesture)` over the ten gesture values is a `cond` +;;;; of keyword equalities. `get-gesture-detected` answers an `rl/Gesture` and +;;;; `(= g :tap)` compares against a member by name, checked at compile time — +;;;; so a typo here is an error and the C's `default: break` has nothing to +;;;; catch. +;;;; +;;;; A slot that has not been written yet holds a zero-length string, because a +;;;; `defvar` with no initialiser is all-bytes-zero and a string is ptr+len — +;;;; a null pointer with a length of 0. draw-text draws nothing for it. That is +;;;; the C's `{ "" }` initialiser arriving by a different route, and it is why +;;;; resetting the log below only has to reset the counter. + +(import rl "vendor:raylib") + +(defconst screen-width 800) +(defconst screen-height 450) + +(defconst max-gesture-strings 20) + +(defvar gesture-log [max-gesture-strings string]) +(defvar gestures-count i32) +(defvar current-gesture rl/Gesture) +(defvar last-gesture rl/Gesture) + +;; The C's switch, as a function. `:else` is its `default:` — an unnamed +;; gesture logs the empty string, which draws nothing, which is what falling +;; through the C's switch without a TextCopy leaves in the slot. +(defn gesture-name [g rl/Gesture] string + (cond + (= g :tap) "GESTURE TAP" + (= g :double-tap) "GESTURE DOUBLETAP" + (= g :hold) "GESTURE HOLD" + (= g :drag) "GESTURE DRAG" + (= g :swipe-right) "GESTURE SWIPE RIGHT" + (= g :swipe-left) "GESTURE SWIPE LEFT" + (= g :swipe-up) "GESTURE SWIPE UP" + (= g :swipe-down) "GESTURE SWIPE DOWN" + (= g :pinch-in) "GESTURE PINCH IN" + (= g :pinch-out) "GESTURE PINCH OUT" + :else "")) + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - input gestures") + (defer (rl/close-window)) + + (rl/set-target-fps 60) + + (let [touch-area (rl/Rectangle {:x 220.0 :y 10.0 + :width (- (f32 screen-width) 230.0) + :height (- (f32 screen-height) 20.0)})] + (until (rl/window-should-close?) + ;; Update + (set last-gesture current-gesture) + (set current-gesture (rl/get-gesture-detected)) + (let [touch (rl/get-touch-position 0)] + + (when (and (rl/collision-point-rec? touch touch-area) + (not (= current-gesture :none)) + (not (= current-gesture last-gesture))) + (set (at gesture-log gestures-count) (gesture-name current-gesture)) + (set gestures-count (+ gestures-count 1)) + ;; Full: start over. The stale slots below the counter are never + ;; drawn, so they do not have to be cleared the way the C clears + ;; them — the loop below is bounded by gestures-count. + (when (>= gestures-count max-gesture-strings) + (set gestures-count 0))) + + ;; Draw + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + + (rl/draw-rectangle-rec touch-area rl/gray) + (rl/draw-rectangle 225 15 (- screen-width 240) (- screen-height 30) + rl/raywhite) + + (rl/draw-text "GESTURES TEST AREA" (- screen-width 270) + (- screen-height 40) 20 (rl/fade rl/gray 0.5)) + + (dotimes [i gestures-count] + (if (= 0 (% i 2)) + (rl/draw-rectangle 10 (+ 30 (* 20 i)) 200 20 + (rl/fade rl/lightgray 0.5)) + (rl/draw-rectangle 10 (+ 30 (* 20 i)) 200 20 + (rl/fade rl/lightgray 0.3))) + ;; The newest entry in maroon and the rest in dark grey, as the C + ;; has it. + (rl/draw-text (at gesture-log i) 35 (+ 36 (* 20 i)) 10 + (if (< i (- gestures-count 1)) rl/darkgray rl/maroon))) + + (rl/draw-rectangle-lines 10 29 200 (- screen-height 50) rl/gray) + (rl/draw-text "DETECTED GESTURES" 50 15 10 rl/gray) + + (unless (= current-gesture :none) + (rl/draw-circle-v touch 30.0 rl/maroon)) + + (rl/end-drawing))))) diff --git a/examples/core-input-keys.flan b/examples/core-input-keys.flan new file mode 100644 index 0000000..9f8c95d --- /dev/null +++ b/examples/core-input-keys.flan @@ -0,0 +1,47 @@ +;;;; raylib [core] example - input keys +;;;; +;;;; examples/core/core_input_keys.c. Faithful, and the only thing worth noting +;;;; is a Flan rule rather than a raylib one. +;;;; +;;;; The C moves the ball by writing `ballPosition.x += 2.0f` on a local +;;;; struct. Flan has the same thing — a local IS an assignable place +;;;; (spec-memory.md) — but the local has to be a `defvar` here rather than a +;;;; `let` inside the loop, because a `let` binding is rebound every iteration +;;;; and the position has to survive between frames. The C's variable is +;;;; outside its while loop for the same reason; this is that, spelled with the +;;;; storage the language has. +;;;; +;;;; `(set (.x ball) ...)` works on a struct field of a global: a field of an +;;;; assignable place is itself one. + +(import rl "vendor:raylib") + +(defconst screen-width 800) +(defconst screen-height 450) + +(defvar ball rl/Vector2) + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - input keys") + (defer (rl/close-window)) + + (set ball (rl/Vector2 {:x (f32 (/ screen-width 2)) + :y (f32 (/ screen-height 2))})) + + (rl/set-target-fps 60) + + (until (rl/window-should-close?) + ;; Update. key-down? and not key-pressed?: this is meant to repeat for as + ;; long as the key is held, which is the whole difference between the two. + (when (rl/key-down? :right) (set (.x ball) (+ (.x ball) 2.0))) + (when (rl/key-down? :left) (set (.x ball) (- (.x ball) 2.0))) + (when (rl/key-down? :up) (set (.y ball) (- (.y ball) 2.0))) + (when (rl/key-down? :down) (set (.y ball) (+ (.y ball) 2.0))) + + ;; Draw + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + (rl/draw-text "move the ball with arrow keys" 10 10 20 rl/darkgray) + (rl/draw-circle-v ball 50.0 rl/maroon) + (rl/end-drawing))) diff --git a/examples/core-input-mouse-wheel.flan b/examples/core-input-mouse-wheel.flan new file mode 100644 index 0000000..5d2c702 --- /dev/null +++ b/examples/core-input-mouse-wheel.flan @@ -0,0 +1,56 @@ +;;;; raylib [core] example - input mouse wheel +;;;; +;;;; examples/core/core_input_mouse_wheel.c. Needed get-mouse-wheel-move, now +;;;; bound. +;;;; +;;;; This is the first example with a `TextFormat` in it, and therefore the +;;;; first that runs into the gap: the C draws +;;;; +;;;; DrawText(TextFormat("Box position Y: %03i", boxPositionY), ...) +;;;; +;;;; and Flan has no way to make a string out of a number. The label is drawn +;;;; with draw-text and the number after it with draw-int-padded from +;;;; examples/digits.flan, which is one glyph per digit out of a [10 string] +;;;; table. The "%03i" is the `3` argument — the leading zeroes are there, and +;;;; a number wider than three digits is drawn in full, exactly as printf's +;;;; minimum-width means. +;;;; +;;;; The position is signed and goes negative as soon as the box scrolls past +;;;; the top, which is why draw-int-padded handles a sign at all. + +(import rl "vendor:raylib") +(import d "digits.flan") + +(defconst screen-width 800) +(defconst screen-height 450) + +(defconst scroll-speed 4) + +(defvar box-y i32) + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - input mouse wheel") + (defer (rl/close-window)) + + (set box-y (- (/ screen-height 2) 40)) + + (rl/set-target-fps 60) + + (until (rl/window-should-close?) + ;; Update. The wheel reading is a per-frame delta, so it is 0.0 on every + ;; frame the wheel did not turn and this is a no-op then. + (set box-y (- box-y (i32 (* (rl/get-mouse-wheel-move) (f32 scroll-speed))))) + + ;; Draw + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + (rl/draw-rectangle (- (/ screen-width 2) 40) box-y 80 80 rl/maroon) + (rl/draw-text "Use mouse wheel to move the cube up and down!" 10 10 20 + rl/gray) + ;; The two halves of the C's one TextFormat call: the literal, measured, + ;; and then the number starting where it ended. + (rl/draw-text "Box position Y: " 10 40 20 rl/lightgray) + (d/draw-int-padded box-y 3 (+ 10 (rl/measure-text "Box position Y: " 20)) + 40 20 rl/lightgray) + (rl/end-drawing))) diff --git a/examples/core-input-mouse.flan b/examples/core-input-mouse.flan new file mode 100644 index 0000000..a6d5d51 --- /dev/null +++ b/examples/core-input-mouse.flan @@ -0,0 +1,61 @@ +;;;; raylib [core] example - input mouse +;;;; +;;;; examples/core/core_input_mouse.c. Needed three new bindings — show-cursor, +;;;; hide-cursor and cursor-hidden? — which are now in vendor/raylib. +;;;; +;;;; The C's `else if` chain over the seven mouse buttons is a `cond` here. +;;;; That is not a workaround: `cond` is what Flan has and it is the same +;;;; first-match-wins shape, with `:else` where C falls off the end. The order +;;;; matters in both — two buttons pressed on the same frame give the earlier +;;;; one, which is the C's behaviour and not an accident of the port. +;;;; +;;;; The colour has to be a `defvar` rather than a `let`, for the reason +;;;; core-input-keys' position does: it is state between frames. + +(import rl "vendor:raylib") + +(defconst screen-width 800) +(defconst screen-height 450) + +(defvar ball-color rl/Color) + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - input mouse") + (defer (rl/close-window)) + + ;; The C also initialises the ball's POSITION, to {-100,-100} so it starts + ;; off-screen. That is dead there and is not here: the first thing the loop + ;; does is overwrite it with get-mouse-position, before anything is drawn. + (set ball-color rl/darkblue) + + (rl/set-target-fps 60) + + (until (rl/window-should-close?) + ;; Update + (when (rl/key-pressed? :h) + (if (rl/cursor-hidden?) (rl/show-cursor) (rl/hide-cursor))) + + (let [ball (rl/get-mouse-position)] + (set ball-color + (cond + (rl/mouse-button-pressed? :left) rl/maroon + (rl/mouse-button-pressed? :middle) rl/lime + (rl/mouse-button-pressed? :right) rl/darkblue + (rl/mouse-button-pressed? :side) rl/purple + (rl/mouse-button-pressed? :extra) rl/yellow + (rl/mouse-button-pressed? :forward) rl/orange + (rl/mouse-button-pressed? :back) rl/beige + :else ball-color)) + + ;; Draw + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + (rl/draw-circle-v ball 40.0 ball-color) + (rl/draw-text "move ball with mouse and click mouse button to change color" + 10 10 20 rl/darkgray) + (rl/draw-text "Press 'H' to toggle cursor visibility" 10 30 20 rl/darkgray) + (if (rl/cursor-hidden?) + (rl/draw-text "CURSOR HIDDEN" 20 60 20 rl/red) + (rl/draw-text "CURSOR VISIBLE" 20 60 20 rl/lime)) + (rl/end-drawing)))) diff --git a/examples/core-input-multitouch.flan b/examples/core-input-multitouch.flan new file mode 100644 index 0000000..4bfc7b4 --- /dev/null +++ b/examples/core-input-multitouch.flan @@ -0,0 +1,69 @@ +;;;; raylib [core] example - input multitouch +;;;; +;;;; examples/core/core_input_multitouch.c. No new bindings: the whole touch +;;;; surface was already there for sand.flan's read-out. +;;;; +;;;; The C keeps `Vector2 touchPositions[MAX_TOUCH_POINTS]`, and this is the +;;;; first example that needs a fixed array whose element type is a STRUCT +;;;; rather than a number. That works — `[10 rl/Vector2]` is ten Vector2s laid +;;;; out flat, no headers, and `(at touch-positions i)` is a place that can be +;;;; assigned a whole struct. Nothing in the repository used one before, so it +;;;; is worth saying that it does. +;;;; +;;;; It is a top-level `defvar` and not a local, which is NOT a stylistic +;;;; choice. A `let` binding takes no type annotation, so the only way to make +;;;; a fixed array inside a function is to initialise it from a literal with +;;;; every element written out — ten `(rl/Vector2 {:x 0.0 :y 0.0})`s here, and +;;;; thirty-two in the gestures testbed. A zeroed local array of a given type +;;;; cannot be spelled. Static storage is what the C's `= { 0 }` gets anyway. +;;;; +;;;; On a desktop with no touchscreen get-touch-point-count is 0 and this draws +;;;; nothing at all, which is correct and is also what the C does. The window +;;;; opening and the help text appearing is the whole of what can be checked +;;;; without hardware. + +(import rl "vendor:raylib") +(import d "digits.flan") + +(defconst screen-width 800) +(defconst screen-height 450) + +(defconst max-touch-points 10) + +(defvar touch-positions [max-touch-points rl/Vector2]) + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - input multitouch") + (defer (rl/close-window)) + + (rl/set-target-fps 60) + + (until (rl/window-should-close?) + ;; Update + (let [count (min max-touch-points (rl/get-touch-point-count))] + (dotimes [i count] + (set (at touch-positions i) (rl/get-touch-position i))) + + ;; Draw + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + + (dotimes [i count] + (let [p (at touch-positions i)] + ;; raylib reports (0,0) for a slot that is not being touched, so the + ;; C filters on it and so does this. It means a real touch in the + ;; very top-left corner is dropped; that is raylib's ambiguity, not + ;; something the port introduced. + (when (and (> (.x p) 0.0) (> (.y p) 0.0)) + (rl/draw-circle-v p 34.0 rl/orange) + ;; The C's TextFormat("%d", i) — one digit, drawn by the shared + ;; helper rather than by a codepoint call, so every number on + ;; screen in these ten files goes through the same path. + (d/draw-int i (- (i32 (.x p)) 10) (- (i32 (.y p)) 70) 40 + rl/black)))) + + (rl/draw-text "touch the screen at multiple locations to get multiple balls" + 10 10 20 rl/darkgray) + + (rl/end-drawing)))) diff --git a/examples/core-input-virtual-controls.flan b/examples/core-input-virtual-controls.flan new file mode 100644 index 0000000..81bc7d3 --- /dev/null +++ b/examples/core-input-virtual-controls.flan @@ -0,0 +1,195 @@ +;;;; raylib [core] example - input virtual controls +;;;; +;;;; examples/core/core_input_virtual_controls.c. No new bindings. +;;;; +;;;; Split the way sand.flan is split: everything above the second banner is +;;;; arithmetic with no raylib calls in it, so test/programs/virtual-controls- +;;;; headless.flan can import this file as a package, drive the pointer over a +;;;; scripted path and hash where the player ended up. That is the only one of +;;;; the ten examples with a headless half, and it has one because it is the +;;;; only one whose interesting part — which D-pad button is under the pointer, +;;;; and what that does to the player — is a function of numbers rather than of +;;;; a frame buffer. The link follows what the program reaches, so the headless +;;;; driver pulls in neither a window nor libraylib. +;;;; +;;;; Two language notes, both visible below. +;;;; +;;;; **No `break`.** The C's nearest-button search is +;;;; +;;;; for (i = 0; i < BUTTON_MAX; i++) { if (...) { pressedButton = i; break; } } +;;;; +;;;; and `(break)` is refused: "break is not implemented yet (see the build +;;;; sequence in plan.org)". `return` from the function works and is what this +;;;; uses — which is why the search is its own `defn` and not written inline +;;;; the way the C has it. That is not a loss: the function is the thing the +;;;; headless case wants to call anyway. Where a loop genuinely cannot be a +;;;; function, the shape left is a flag and a compound loop condition. +;;;; +;;;; **No `fabsf`.** The prelude has sqrt-f32 and sign-f32 but no absolute +;;;; value for floats, and the comment there says why for integers. `(max v +;;;; (- 0.0 v))` is it, and unlike the integer case it has no edge: there is no +;;;; float whose negation is itself except -0.0, whose absolute value is 0.0 +;;;; either way. +;;;; +;;;; The button geometry is `defconst` arrays of struct literals. That works — +;;;; and it is the only way a fixed array can be made inside anything but a +;;;; top-level `defvar`, since a `let` binding takes no type annotation. + +(import rl "vendor:raylib") +(import d "digits.flan") + +(defconst screen-width 800) +(defconst screen-height 450) + +;; The C's PadButton enum. -1 is BUTTON_NONE and the four directions are 0..3, +;; in the order the arrays below are written. A `defenum` would not help: the +;; value is used as an index and an enum does not convert to an integer +;; ("i32 converts a number, found ..."), so it would have to be turned back +;; into one at every use. +(defconst button-none -1) +(defconst button-up 0) +(defconst button-left 1) +(defconst button-right 2) +(defconst button-down 3) +(defconst button-max 4) + +(defconst button-radius f32 30.0) + +;; padPosition is {100, 350} and the buttons sit a radius and a half away from +;; it on each axis. The C computes these at run time from padPosition; they are +;; written out here because a defconst is compile-time and the numbers are. +(defconst button-positions [button-max rl/Vector2] + [(rl/Vector2 {:x 100.0 :y 305.0}) ; up + (rl/Vector2 {:x 55.0 :y 350.0}) ; left + (rl/Vector2 {:x 145.0 :y 350.0}) ; right + (rl/Vector2 {:x 100.0 :y 395.0})]) ; down + +(defconst player-speed f32 75.0) + +;;;; ── The part with no raylib in it ────────────────────────────────── + +(defvar player rl/Vector2) + +(defn reset-player [] + (set player (rl/Vector2 {:x (/ (f32 screen-width) 2.0) + :y (/ (f32 screen-height) 2.0)}))) + +(defn abs-f32 [v f32] f32 + (max v (- 0.0 v))) + +;; The C's search, with `return` where it had `break`. Manhattan distance and +;; not Euclidean — the C adds the two axis distances and compares against the +;; radius — so the hit area is a diamond, which is deliberate: it makes the +;; four buttons tile without overlapping. +(defn nearest-button [pos rl/Vector2] i32 + (dotimes [i button-max] + (let [b (at button-positions i) + dist-x (abs-f32 (- (.x b) (.x pos))) + dist-y (abs-f32 (- (.y b) (.y pos)))] + (when (< (+ dist-x dist-y) button-radius) + (return i)))) + button-none) + +;; The C's switch on the pressed button. Nothing moves for button-none, which +;; is the `default: break`. +(defn move-player [button i32 dt f32] + (let [step (* player-speed dt)] + (cond + (= button button-up) (set (.y player) (- (.y player) step)) + (= button button-left) (set (.x player) (- (.x player) step)) + (= button button-right) (set (.x player) (+ (.x player) step)) + (= button button-down) (set (.y player) (+ (.y player) step)) + :else (do)))) + +;; FNV-1a over the player's two floats, so the headless driver has one number +;; to compare. The floats are read through their bit patterns rather than +;; rounded to integers: rounding would hide exactly the kind of drift — a +;; crossed x and y, a step applied twice — the case exists to catch. +(defconst fnv-offset u64 0xcbf29ce484222325) +(defconst fnv-prime u64 1099511628211) + +(defn hash-f32 [h u64 v f32] u64 + ;; No float-to-bits cast in the language, so the value is scaled and + ;; truncated instead. 1024 keeps three decimal places of a screen + ;; coordinate, which is finer than any real difference between two correct + ;; runs and coarser than the last bit of an f32 — so this is reproducible + ;; across targets where a raw bit pattern would depend on the FPU. + (let [n (i64 (* v 1024.0)) + g h] + (dotimes [b 8] + (set g (bit-xor g (u64 (bit-and (>> n (i64 (* b 8))) 255)))) + (set g (* g fnv-prime))) + g)) + +(defn hash-player [] u64 + (hash-f32 (hash-f32 fnv-offset (.x player)) (.y player))) + +;;;; ── The raylib front-end ─────────────────────────────────────────── + +;; The arrowheads, one triangle of three points per button, in the winding +;; raylib wants — counter-clockwise, or it culls them and draws nothing. A +;; [4 [3 rl/Vector2]]: a fixed array of fixed arrays of a struct, which is the +;; deepest shape any of these ten examples asks for and which works. +(defconst arrow-tris [button-max [3 rl/Vector2]] + [[(rl/Vector2 {:x 100.0 :y 293.0}) + (rl/Vector2 {:x 91.0 :y 314.0}) + (rl/Vector2 {:x 109.0 :y 314.0})] + [(rl/Vector2 {:x 64.0 :y 341.0}) + (rl/Vector2 {:x 43.0 :y 350.0}) + (rl/Vector2 {:x 64.0 :y 359.0})] + [(rl/Vector2 {:x 157.0 :y 350.0}) + (rl/Vector2 {:x 136.0 :y 341.0}) + (rl/Vector2 {:x 136.0 :y 359.0})] + [(rl/Vector2 {:x 91.0 :y 386.0}) + (rl/Vector2 {:x 100.0 :y 407.0}) + (rl/Vector2 {:x 109.0 :y 386.0})]]) + +(defconst label-colors [button-max rl/Color] + [(rl/Color {:r 253 :g 249 :b 0 :a 255}) ; yellow, up + (rl/Color {:r 0 :g 121 :b 241 :a 255}) ; blue, left + (rl/Color {:r 230 :g 41 :b 55 :a 255}) ; red, right + (rl/Color {:r 0 :g 228 :b 48 :a 255})]) ; green, down + +(defn main [] + (rl/init-window screen-width screen-height + "raylib [core] example - input virtual controls") + (defer (rl/close-window)) + + (reset-player) + (rl/set-target-fps 60) + + (until (rl/window-should-close?) + ;; Update. Touch first, mouse as the desktop stand-in — and on the desktop + ;; the left button has to be held, or the player would follow the cursor + ;; wherever it went. + (let [touching (> (rl/get-touch-point-count) 0) + input (if touching + (rl/get-touch-position 0) + (rl/get-mouse-position)) + pressed (if (or touching (rl/mouse-button-down? :left)) + (nearest-button input) + button-none)] + + (move-player pressed (rl/get-frame-time)) + + ;; Draw + (rl/begin-drawing) + (rl/clear-background rl/raywhite) + + (rl/draw-circle-v player 50.0 rl/maroon) + + (dotimes [i button-max] + (rl/draw-circle-v (at button-positions i) button-radius + (if (= i pressed) rl/darkgray rl/black)) + (let [t (at arrow-tris i)] + (rl/draw-triangle (at t 0) (at t 1) (at t 2) + (at label-colors i)))) + + (rl/draw-text "move the player with D-Pad buttons" 10 10 20 rl/darkgray) + ;; Not in the C: which button the search picked, as a number, so the + ;; headless case and the window agree about the same thing. + (rl/draw-text "button: " 10 34 20 rl/lightgray) + (d/draw-int pressed (+ 10 (rl/measure-text "button: " 20)) 34 20 + rl/lightgray) + + (rl/end-drawing)))) diff --git a/examples/digits.flan b/examples/digits.flan new file mode 100644 index 0000000..15c9afc --- /dev/null +++ b/examples/digits.flan @@ -0,0 +1,127 @@ +;;;; Drawing a number, because the language cannot make one into a string. +;;;; +;;;; Five of the ten ported examples call raylib's `TextFormat` to put a number +;;;; on the screen. Flan has no string formatting and no way to reach it: +;;;; +;;;; - `i64->bytes` is a builtin and answers a `[u8]`; +;;;; - `draw-text` takes a `string`; +;;;; - nothing converts a `[u8]` into a `string`. A string is a compile-time +;;;; literal or a parameter, and there is no allocator to build one in. +;;;; +;;;; `TextFormat` itself is not bindable either, and not because of the FFI +;;;; rules: it is variadic, so its signature is not a signature — declaring it +;;;; with fixed arguments would be a claim about the ABI that is false on every +;;;; target at once, and it returns a `char *` into a rotating static buffer, +;;;; which `declare-c` refuses by name anyway ("a string only crosses as a +;;;; parameter — a C function that *returns* one returns something Flan has no +;;;; owner for"). +;;;; +;;;; So a number reaches the screen one digit at a time, each digit drawn as a +;;;; one-character `string` out of the table below. sand.flan already does this +;;;; for a single digit with `draw-text-codepoint`; this is the same trick +;;;; generalised, in one place, so the gap shows up in the report as one gap +;;;; rather than as five separate improvisations. +;;;; +;;;; Everything here needs a window: `measure-text` answers 0 for every string +;;;; until init-window has loaded the default font, and a zero advance would +;;;; stack every digit on top of the first. + +(import rl "vendor:raylib") + +;; A `[10 string]` — a fixed array whose element type is `string`. That works, +;; which is worth recording: a string is ptr+len and the array is ten of those +;; laid out flat, with the bytes themselves in the module's constant data. +(defconst digit-glyphs [10 string] + ["0" "1" "2" "3" "4" "5" "6" "7" "8" "9"]) + +(defconst minus-glyph "-") +(defconst dot-glyph ".") + +;; One glyph, and how far the pen moved. raylib's default font is not +;; monospaced — "1" is narrower than "8" — so the advance is measured rather +;; than assumed, which is also what keeps the spacing identical to what +;; draw-text would have produced for the whole string at once. +(defn draw-glyph [g string x i32 y i32 size i32 color rl/Color] i32 + (rl/draw-text g x y size color) + (rl/measure-text g size)) + +;; How many decimal digits `n` has, for n >= 0. 0 has one. +(defn digit-count [n i32] i32 + (let [d 1 + r (/ n 10)] + (while (> r 0) + (set d (+ d 1)) + (set r (/ r 10))) + d)) + +(defn pow10 [e i32] i32 + (let [p 1] + (dotimes [i e] + (set p (* p 10))) + p)) + +;; The whole point of the file. Answers the width drawn, so a caller can put +;; something after it — which is how the `TextFormat("%s: %i", ...)` shapes in +;; the C are reassembled here: draw the literal part with draw-text, then this +;; at x plus its width. +;; +;; Negative numbers get the sign and then the magnitude. i32's most negative +;; value is NOT handled: negating it wraps to itself, so it would print its own +;; bit pattern with a minus in front. Nothing here ever reaches it — these are +;; screen coordinates, frame counts and axis readings — and guarding it would +;; be a branch that no call site can take. +(defn draw-int [n i32 x i32 y i32 size i32 color rl/Color] i32 + (let [cx x + v n] + (when (< v 0) + (set cx (+ cx (draw-glyph minus-glyph cx y size color))) + (set v (- 0 v))) + (let [count (digit-count v)] + (dotimes [i count] + (let [d (% (/ v (pow10 (- (- count 1) i))) 10)] + (set cx (+ cx (draw-glyph (at digit-glyphs d) cx y size color)))))) + (- cx x))) + +;; The same with a fixed number of leading zeroes — the C's "%03i". A number +;; wider than `width` is drawn in full rather than truncated, which is what +;; printf does too. +(defn draw-int-padded [n i32 width i32 x i32 y i32 size i32 color rl/Color] + i32 + (let [cx x + v n] + (when (< v 0) + (set cx (+ cx (draw-glyph minus-glyph cx y size color))) + (set v (- 0 v))) + (let [count (max width (digit-count v))] + (dotimes [i count] + (let [d (% (/ v (pow10 (- (- count 1) i))) 10)] + (set cx (+ cx (draw-glyph (at digit-glyphs d) cx y size color)))))) + (- cx x))) + +;; "%.02f" and friends. `places` digits after the point, rounded by adding a +;; half at that scale before the split — so 0.999 at two places is "1.00" and +;; not "0.99", which is what the C's printf would have done and what a reader +;; comparing the two screens would expect. +;; +;; f32 and not f64 deliberately: every number this draws comes out of raylib, +;; and raylib's are floats. Widening them here would suggest a precision the +;; value does not have. +(defn draw-f32 [v f32 places i32 x i32 y i32 size i32 color rl/Color] i32 + (let [cx x + av v] + (when (< av 0.0) + (set cx (+ cx (draw-glyph minus-glyph cx y size color))) + (set av (- 0.0 av))) + (let [scale (pow10 places) + ;; The rounding and the split happen in one integer so the two halves + ;; cannot disagree: rounding them separately is how "0.999" becomes + ;; "0.100" — the fraction carries and the whole part does not hear + ;; about it. + total (i32 (+ (* av (f32 scale)) 0.5)) + whole (/ total scale) + frac (% total scale)] + (set cx (+ cx (draw-int whole cx y size color))) + (when (> places 0) + (set cx (+ cx (draw-glyph dot-glyph cx y size color))) + (set cx (+ cx (draw-int-padded frac places cx y size color))))) + (- cx x))) diff --git a/test/dune b/test/dune index 914ab8b..adcc278 100644 --- a/test/dune +++ b/test/dune @@ -14,6 +14,10 @@ (glob_files %{workspace_root}/vendor/agent/*) ; The EDN tokenizer, which programs/edn.flan imports. (glob_files %{workspace_root}/vendor/edn/*) + ; The ported raylib examples. Only one of them has a headless acceptance + ; case, but it imports its example as a package and that example imports + ; examples/digits.flan, so the directory has to be here whole. + (glob_files %{workspace_root}/examples/*) (glob_files programs/*.flan) ; The reload primitive's host: a C main that dlopens what Build.shared made. (file reload_host.c) diff --git a/test/programs/virtual-controls-headless.flan b/test/programs/virtual-controls-headless.flan new file mode 100644 index 0000000..cb39acd --- /dev/null +++ b/test/programs/virtual-controls-headless.flan @@ -0,0 +1,59 @@ +;;;; core-input-virtual-controls.flan's other half: no window, a scripted +;;;; pointer, hash where the player ended up. +;;;; +;;;; The same split sand.flan and sand-headless.flan already have, and it is +;;;; available here for the same reason: the interesting part of that example — +;;;; which D-pad button is under the pointer, and what a held button does to +;;;; the player — is arithmetic over numbers, so it runs with no window, no GL +;;;; context and no libraylib. The link follows what the program reaches: +;;;; nothing below calls into raylib, so no shim is compiled and no -lraylib is +;;;; passed, and the example's own drawing functions are never emitted — which +;;;; was checked with ldd on the built binary: libm and libc and nothing else. +;;;; The example's `main` is not exported, so the only main is this one. +;;;; +;;;; Unlike sand-headless there is no wasm32 case for it yet. Nothing here +;;;; should stop one, for exactly the reason above — but it has not been run, +;;;; so it is not claimed. +;;;; +;;;; It is the only one of the ten ported examples with a headless case, and +;;;; the honest reason is that it is the only one that earns it. The other nine +;;;; are input read straight into drawing calls: a test of them would assert +;;;; that raylib answers 0 for every input with no window open, which is also +;;;; what a binding with its arguments crossed would answer. +;;;; +;;;; What this DOES pin: that the four buttons are where the geometry says they +;;;; are, that the diamond hit test uses x and y the right way round, and that +;;;; the four directions move the player the four different ways. A crossed +;;;; axis anywhere in nearest-button or move-player changes the hash. + +(import vc "../../examples/core-input-virtual-controls.flan") +(import rl "vendor:raylib") + +;; A sixtieth of a second, fixed. The real program uses get-frame-time, which +;; is exactly the thing a headless run has no answer for. +(defconst dt f32 0.0166666) + +;; A lawnmower sweep over the pad's corner of the screen, two pixels at a time. +;; It crosses all four buttons and the dead space between them, so every branch +;; of nearest-button is taken and button-none is taken most of all. +(defconst sweep-x0 20) +(defconst sweep-x1 180) +(defconst sweep-y0 280) +(defconst sweep-y1 420) +(defconst sweep-step 2) + +(defn main [] i32 + (vc/reset-player) + (let [y sweep-y0] + (while (<= y sweep-y1) + (let [x sweep-x0] + (while (<= x sweep-x1) + (let [p (rl/Vector2 {:x (f32 x) :y (f32 y)})] + (vc/move-player (vc/nearest-button p) dt)) + (set x (+ x sweep-step)))) + (set y (+ y sweep-step)))) + ;; write-stdout and i64->bytes are builtins (lib/check.ml), not prelude + ;; functions, so this does not go through the printers. + (write-stdout (i64->bytes (i64 (vc/hash-player)))) + (write-stdout (bytes "\n")) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index ee57812..8240050 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -530,6 +530,23 @@ let () = outputs "sand, headless" "programs/sand-headless.flan" sand_out; outputs ~opt:"-O0" "sand, headless, -O0" "programs/sand-headless.flan" sand_out; + (* The ported raylib example that has a headless half. The other nine of + the ten in examples/ are input read straight into drawing calls, and a + test of those would be asserting that raylib answers 0 for every input + with no window open — which is also what a binding with its arguments + crossed would answer. This one is different: which virtual D-pad button + sits under a pointer, and what a held button does to the player, is + arithmetic. The driver sweeps a pointer over the pad in a fixed grid, + so every branch of the search is taken, and hashes where the player + ended up. A crossed x and y anywhere in it changes the number. Like + sand-headless it imports the example and reaches no raylib call, so it + links neither a shim nor libraylib. *) + let vc_out = "-2146089238186896844\n" in + outputs "virtual controls, headless" + "programs/virtual-controls-headless.flan" vc_out; + outputs ~opt:"-O0" "virtual controls, headless, -O0" + "programs/virtual-controls-headless.flan" vc_out; + outputs ~opt:"-O0" "value semantics, -O0" "programs/values.flan" values_out; outputs ~opt:"-O0" "machine surface, -O0" "programs/machine.flan" machine_out; diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index a406210..d6b792c 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -68,6 +68,23 @@ (declare-c set-target-fps [fps i32] "SetTargetFPS") (declare-c set-trace-log-level [level TraceLogLevel] "SetTraceLogLevel") +;; A bitfield, like the gestures below and for the same reason: raylib wants +;; the OR of several and a keyword can only ever name one member, so the +;; parameter is a u32 and the members are defconsts rather than a defenum. +;; +;; The part that is NOT obvious from the signature: this has to be called +;; BEFORE init-window. raylib stores the flags and reads them while creating +;; the context, so setting them afterwards is accepted, logged at INFO, and +;; does nothing to the window that already exists — which looks exactly like +;; a binding that did not work. Only the four the examples ask for are here; +;; the rest are one line each when something calls them. +(defconst flag-fullscreen-mode u32 2) +(defconst flag-window-resizable u32 4) +(defconst flag-msaa-4x-hint u32 32) +(defconst flag-vsync-hint u32 64) + +(declare-c set-config-flags [flags u32] "SetConfigFlags") + ;; ── Input ─────────────────────────────────────────────────────────── (declare-c key-pressed? [key Key] bool "IsKeyPressed") @@ -84,6 +101,21 @@ (declare-c get-mouse-position [] Vector2 "GetMousePosition") +;; One notch of the wheel is 1.0 and there are no fractional notches on an +;; ordinary mouse, but it is a float because a trackpad's two-finger scroll +;; is continuous. It is the DELTA since the last frame, not an accumulated +;; position, so it reads 0.0 on every frame the wheel did not move — which is +;; why a caller that wants a running total keeps one itself. +(declare-c get-mouse-wheel-move [] f32 "GetMouseWheelMove") + +;; The cursor's visibility is window state and not input, but it is read and +;; written by the same code that reads the mouse, so it sits here. +;; hide-cursor only hides it; it does not lock it to the window, which is +;; what raylib's separate DisableCursor does and which nothing here needs. +(declare-c show-cursor [] "ShowCursor") +(declare-c hide-cursor [] "HideCursor") +(declare-c cursor-hidden? [] bool "IsCursorHidden") + ;; ── Colours ───────────────────────────────────────────────────────── ;; ;; A Color is four bytes in RGBA order, so it is *not* the little-endian @@ -92,9 +124,52 @@ (declare-c get-color [hex u32] Color "GetColor") +;; The same colour at a different alpha: raylib multiplies `a` by the factor +;; and leaves r, g and b alone. It is NOT a blend against a background, so a +;; faded colour still needs something drawn behind it to fade against. Bound +;; because the gesture examples draw every overlay through it, and it is the +;; only call in the file that takes a Color and answers one — which makes it +;; the shortest statement anywhere that the Color crossing works in both +;; directions at once. +(declare-c fade [color Color alpha f32] Color "Fade") + (defconst black (Color {:r 0 :g 0 :b 0 :a 255})) (defconst white (Color {:r 255 :g 255 :b 255 :a 255})) +;; raylib's own named palette, from raylib.h's CLITERAL macros. These are the +;; only entries in this file that are not a function, a layout or an enum, and +;; they are here for the reason the two above already were: every raylib +;; example is written in terms of them, so without them a port is a wall of +;; hex that cannot be diffed against the C it came from. They are values and +;; not calls — a Color is four bytes with no packing question — so unlike +;; get-color they cost nothing at run time and need no window. +(defconst lightgray (Color {:r 200 :g 200 :b 200 :a 255})) +(defconst gray (Color {:r 130 :g 130 :b 130 :a 255})) +(defconst darkgray (Color {:r 80 :g 80 :b 80 :a 255})) +(defconst yellow (Color {:r 253 :g 249 :b 0 :a 255})) +(defconst gold (Color {:r 255 :g 203 :b 0 :a 255})) +(defconst orange (Color {:r 255 :g 161 :b 0 :a 255})) +(defconst pink (Color {:r 255 :g 109 :b 194 :a 255})) +(defconst red (Color {:r 230 :g 41 :b 55 :a 255})) +(defconst maroon (Color {:r 190 :g 33 :b 55 :a 255})) +(defconst green (Color {:r 0 :g 228 :b 48 :a 255})) +(defconst lime (Color {:r 0 :g 158 :b 47 :a 255})) +(defconst darkgreen (Color {:r 0 :g 117 :b 44 :a 255})) +(defconst skyblue (Color {:r 102 :g 191 :b 255 :a 255})) +(defconst blue (Color {:r 0 :g 121 :b 241 :a 255})) +(defconst darkblue (Color {:r 0 :g 82 :b 172 :a 255})) +(defconst purple (Color {:r 200 :g 122 :b 255 :a 255})) +(defconst violet (Color {:r 135 :g 60 :b 190 :a 255})) +(defconst darkpurple (Color {:r 112 :g 31 :b 126 :a 255})) +(defconst beige (Color {:r 211 :g 176 :b 131 :a 255})) +(defconst brown (Color {:r 127 :g 106 :b 79 :a 255})) +(defconst darkbrown (Color {:r 76 :g 63 :b 47 :a 255})) +(defconst magenta (Color {:r 255 :g 0 :b 255 :a 255})) +;; Alpha 0, so it is invisible rather than a colour — raylib's own name for it. +(defconst blank (Color {:r 0 :g 0 :b 0 :a 0})) +;; raylib's off-white background, which is what every example clears to. +(defconst raywhite (Color {:r 245 :g 245 :b 245 :a 255})) + ;; ── Drawing ───────────────────────────────────────────────────────── (declare-c begin-drawing [] "BeginDrawing") @@ -480,6 +555,14 @@ ;; scale by instead of assuming the target fps was met. (declare-c get-frame-time [] f32 "GetFrameTime") + +;; The measured rate, as an integer, which is not 1/get-frame-time: raylib +;; averages the last handful of frames so the read-out does not flicker. +;; draw-fps was already bound and puts the same number on the screen itself; +;; this is the one that hands it back, for a program that wants to compare it +;; against a target it set. +(declare-c get-fps [] i32 "GetFPS") + (declare-c get-time [] f64 "GetTime") (declare-c get-screen-width [] i32 "GetScreenWidth") (declare-c get-screen-height [] i32 "GetScreenHeight") @@ -537,6 +620,32 @@ [pad i32 axis GamepadAxis] f32 "GetGamepadAxisMovement") +;; An INTEGER-faced version of the call above is wanted and cannot be had, and +;; the pair of refusals that stops it is worth recording here because it is +;; structural rather than incidental. +;; +;; get-gamepad-axis-count answers how many axes the pad reports, and +;; core_input_gamepad's read-out walks 0..count-1 and asks for each. That loop +;; cannot go through the binding above: the index is an i32 and +;; +;; expected rl/GamepadAxis, found i32 +;; +;; — an integer does not convert to an enum, and a keyword names exactly one +;; member so it cannot come from a loop variable either. The obvious fix, a +;; second declare-c of the same symbol with an i32 parameter, is refused too: +;; +;; 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` cannot help, because what has to change is the parameter's +;; TYPE and a wrapper can only rename. Nothing here is wrong: one declaration +;; per symbol is what keeps the generated prototype unique, and an enum that +;; silently accepted integers would give up what makes a keyword argument +;; checkable. The consequence is simply that an enum parameter cannot be +;; indexed, and the caller spells the loop as a cond over the members it +;; knows — which is what examples/core-input-gamepad.flan does. + ;; SetGamepadVibration is NOT bound, and the reason is not the usual one. Two ;; things are wrong with it at once. Its arity changed — 5.1-dev takes three ;; floats and 6.1-dev takes four, there is no 5.5 header here to settle which,