;;;; 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 converts to an integer, when you say so.** 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 that used to end the discussion — ;;;; ;;;; i32 converts a number, found rl/Gesture ;;;; ;;;; — so the three range tests were spelled out as keyword equalities, one ;;;; arm per member. They are `(> (i32 g) 255)`, `(> (i32 g) 15)` and ;;;; `(< (i32 g) 3)` now, still behind the named predicates below, and the fix ;;;; is not brevity: the enumerated version was *wrong about the future*. A ;;;; gesture raylib adds later falls silently out of a list of four members, ;;;; where the C's `> 15` catches it. The range test is the honest reading of ;;;; a bitfield and now it is the one written. ;;;; ;;;; The rule the refusal came from is unchanged and worth keeping: an enum is ;;;; its own type in the checker, so `:tpa` is an error at the call site ;;;; instead of a number that is wrong later, and a bare integer still does not ;;;; fit an `rl/Gesture` parameter. `i32(g)` does not weaken that — it is ;;;; named, and it is at the site. The rule was "an integer must not arrive ;;;; silently", not "an integer is dangerous". ;;;; ;;;; The `!= 4` stays a keyword comparison, `not g == :gesture-hold`. That ;;;; one was never a range test; it is a single member, and the C's 4 is a ;;;; magic number the keyword reads better than. ;;;; ;;;; **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.** A number can be made into a string now — ;;;; (string (i64->bytes n)) — but a *format* still cannot: f64->bytes is ;;;; "%g", with no way to ask for two decimal places. 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.fln 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 `once` or a literal ;;;; with every element written out — thirty-two Vector2s, here. They are ;;;; `once`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.fln" const screen-width = 800 const screen-height = 450 const pi: f32 = 3.14159265 const gesture-log-size = 20 const max-touch-count = 32 once gesture-log: [gesture-log-size str] ;; 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. once gesture-log-index: i32 once previous-gesture: rl/Gesture once last-gesture: rl/Gesture once gesture-color: rl/Color once log-mode: i32 once current-angle: f32 once touch-positions: [max-touch-count rl/Vector2] ;; ── The comparisons the C makes on the raw bitfield ───────────────── ;; ;; A gesture is a flag: 1, 2, 4, 8 … up to 512, so the ranges are the C's way ;; of asking which family a gesture belongs to. (i32 g) is what lets that be ;; written; they are named here rather than inline because a bare 255 at a ;; call site says nothing, and because a gesture raylib adds later lands in ;; the right family without this file being edited. fn is-pinch(g: rl/Gesture) -> bool ; the C's `> 255` i32(g) > 255 fn is-swipe(g: rl/Gesture) -> bool ; the C's `> 15` i32(g) > 15 fn is-tapish(g: rl/Gesture) -> bool ; the C's `< 3` i32(g) < 3 ;; Two orderings these impose, both of them the C's as well. A pinch is above ;; 255 and therefore above 15, so is-swipe has to be asked after the pinches ;; rather than before; and is-tapish admits :gesture-none, which is 0, so it ;; belongs under a :gesture-none guard. The C's switch over single members ;; hides both; a range test cannot. fn gesture-name(g: rl/Gesture) -> str if g == :gesture-none "None" elif g == :gesture-tap "Tap" elif g == :gesture-double-tap "Double Tap" elif g == :gesture-hold "Hold" elif g == :gesture-drag "Drag" elif g == :gesture-swipe-right "Swipe Right" elif g == :gesture-swipe-left "Swipe Left" elif g == :gesture-swipe-up "Swipe Up" elif g == :gesture-swipe-down "Swipe Down" elif g == :gesture-pinch-in "Pinch In" elif g == :gesture-pinch-out "Pinch Out" else "Unknown" fn gesture-color-of(g: rl/Gesture) -> rl/Color if g == :gesture-tap rl/blue elif g == :gesture-double-tap rl/skyblue elif g == :gesture-drag rl/lime ;; The two pinches are above 255 and so are above 15 as well: is-swipe is a ;; range test and has to be asked after them, not before. The C gets this ;; for free by being a switch over single members. elif g == :gesture-pinch-in rl/violet elif g == :gesture-pinch-out rl/orange elif is-swipe(g) rl/red 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 fn is-should-log(g: rl/Gesture) -> bool if g == :gesture-none false elif log-mode == 3 (not g == :gesture-hold and not g == previous-gesture) or is-tapish(g) elif log-mode == 2 not g == :gesture-hold elif log-mode == 1 not g == previous-gesture else true fn push-log(g: rl/Gesture) -> () previous-gesture = g gesture-color = gesture-color-of(g) if gesture-log-index <= 0 gesture-log-index = gesture-log-size gesture-log-index -= 1 gesture-log[gesture-log-index] = gesture-name(g) ;; ── Drawing ───────────────────────────────────────────────────────── const last-x = 165 const last-y = 130 const prot-x: f32 = 266.0 const prot-y: f32 = 315.0 const angle-length: f32 = 90.0 fn swipe-box(gx: i32, gy: i32, which: rl/Gesture) -> () rl/draw-rectangle(gx, gy, 20, 20, if last-gesture == which then rl/red else rl/lightgray) fn 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, :gesture-swipe-up) swipe-box(last-x, last-y + 20, :gesture-swipe-left) swipe-box(last-x + 40, last-y + 20, :gesture-swipe-right) swipe-box(last-x + 20, last-y + 40, :gesture-swipe-down) rl/draw-circle(last-x + 80, last-y + 16, 10.0, if last-gesture == :gesture-tap then rl/blue else 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 == :gesture-drag then rl/lime else rl/lightgray) rl/draw-circle(last-x + 80, last-y + 43, 10.0, if last-gesture == :gesture-double-tap then rl/skyblue else rl/lightgray) rl/draw-circle(last-x + 103, last-y + 43, 10.0, if last-gesture == :gesture-double-tap then rl/skyblue else 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 == :gesture-pinch-out then rl/orange else rl/lightgray let in-c = if last-gesture == :gesture-pinch-in then rl/violet else 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. for i in range(4) rl/draw-circle(last-x + 180, (last-y + 7) + i * 15, 5.0, if touch-count <= i then rl/lightgray else gesture-color) fn draw-log() -> () rl/draw-text("Log", 10, 10, 20, rl/black) ;; Forward from the newest, wrapping — the inverted queue read the right way ;; round. for i in range(gesture-log-size) let ii = (gesture-log-index + i) % gesture-log-size rl/draw-text(gesture-log[ii], 10, 10 + 410 - i * 20, 20, if i == 0 then gesture-color else 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} let b2 = rl/Rectangle{.x 108.0 .y 7.0 .width 36.0 .height 26.0} rl/draw-rectangle-rec(b1, if log-mode == 1 or log-mode == 3 then rl/maroon else 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 log-mode == 2 or log-mode == 3 then rl/maroon else rl/gray) rl/draw-text("Hide", 115, 10, 10, rl/white) rl/draw-text("Hold", 115, 20, 10, rl/white) fn 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) fn 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. gesture-color = rl/black gesture-log-index = gesture-log-size log-mode = 1 rl/set-target-fps(60) let b1 = rl/Rectangle{.x 53.0 .y 7.0 .width 48.0 .height 26.0} let 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() let 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. if (not g == :gesture-none and not g == :gesture-hold) and not g == previous-gesture last-gesture = g ;; The two mode buttons toggle one bit each of log-mode. if rl/is-mouse-button-released(:mouse-left) let m = rl/get-mouse-position() if rl/check-collision-point-rec(m, b1) log-mode = if log-mode == 3 2 elif log-mode == 2 3 elif log-mode == 1 0 else 1 if rl/check-collision-point-rec(m, b2) log-mode = if log-mode == 3 1 elif log-mode == 2 0 elif log-mode == 1 3 else 2 if is-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. if is-pinch(g) current-angle = rl/get-gesture-pinch-angle() elif is-swipe(g) current-angle = rl/get-gesture-drag-angle() elif not g == :gesture-none current-angle = 0.0 else () for i in range(touch-count) touch-positions[i] = rl/get-touch-position(i) ;; Draw rl/with-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 == :gesture-none): if touch-count > 0 for i in range(touch-count) let p = 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. if touch-count == 2 rl/draw-line-ex(touch-positions[0], touch-positions[1], if g == :gesture-pinch-out then 8.0 else 12.0, gesture-color) else 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)