flan/test/programs/raylib-font.flan
Joseph Ferano 52e9c92b0f The enum prefix goes uniform across all eleven
The author's ruling: "I think the prefix reads better, keep it" — so two
prefixed enums out of eleven was the inconsistency, not the prefix.
TraceLogLevel takes log-, CameraProjection projection-, CameraMode camera-,
GamepadButton button-, GamepadAxis axis-, Gesture gesture-, MouseCursor
cursor-, TextureFilter filter-, PixelFormat pixel-.

Two of those are judgement. CameraProjection and CameraMode share raylib's
CAMERA_ and deliberately do not share a Flan prefix: they are two questions
asked of the same struct, and :projection-perspective beside :camera-orbital
says which is being answered. GamepadButton and GamepadAxis take the short
stems rather than a shared gamepad-, which keeps :button-left-face-up and
:axis-left-trigger readable.

It is a reading choice and not a collision fix, and bindings, raylib.flan and
docs/BUILT.md all say so: a keyword resolves against the expected type and
nothing else, so :point at a TextureFilter site was never ambiguous. What the
prefix buys is the call site read on its own.

The three constant exception lines are keyed on the member's full Flan
spelling and moved with it. flan generate-c vendor/raylib is green against
raylib-5.5.h, and the check was confirmed non-vacuous by breaking it:
filter-trilinearr reported TEXTURE_FILTER_TRILINEARR rather than passing.
test_flan pins one member of each of the eleven to the C name the rule
reaches, read out of the real bindings file.

sand.flan line 121 is (rl/set-trace-log-level :warning) and is the author's
to respell. TraceLogLevel carries a warning alias beside log-warning, mapped
by name in bindings, so the suite stays green until he does; FIX.org has the
three-edit removal recipe.
2026-09-21 07:59:48 +07:00

175 lines
9.3 KiB
Plaintext

(import rl "vendor:raylib")
;; raylib's Font family, headless.
;;
;; A previous lane refused this whole family by name and gave a real reason: a
;; Font is three ints beside a Texture2D, a Rectangle* and a GlyphInfo*, a
;; GlyphInfo embeds an Image, and that was two more aggregates and two owned
;; arrays "for something with no headless test at the end of it". The second
;; half of that turned out to be wrong, and this file is the counter-example.
;;
;; **Nothing here makes a font.** That is the whole trick. Every raylib call
;; that produces one needs something a headless run has not got —
;; get-font-default needs init-window, load-font and load-font-ex need a TTF
;; on disk and a GL context to upload the atlas to — so this file *builds* a
;; Font out of Flan arrays, field by field, and hands it to raylib to compute
;; with. raylib's text measuring is pure CPU arithmetic: it walks the glyph
;; array for a codepoint, takes the advance out of the glyph or the width out
;; of the atlas rectangle, and scales by the base size.
;;
;; Which makes this the scalars-in/fields-out shape rather than the
;; store-and-return one. There is no raylib-produced struct anywhere in the
;; file for a permutation to cancel against: Flan chose every field, and what
;; comes back is a number raylib worked out from them.
;;
;; What is pinned, and by what:
;;
;; - **base-size** by the scale factor. Measuring "AB" at size 10 against a
;; base size of 10 is scale 1 and at size 20 is scale 2, and both are
;; asserted, so the field cannot be a constant that happens to fit.
;; - **glyph-count** by the search bound. 'Z' is not in the font, and
;; raylib's miss falls back to index 0 rather than reading past the array.
;; - **recs and glyphs** as pointers, by being read through at all.
;; - **advance-x against offset-x**, because glyph C deliberately has an
;; advance of 0: raylib then falls back to the atlas rectangle's width
;; plus offset-x, so measuring "ABC" reaches three more fields than
;; measuring "AB" does.
;; - **where the Texture2D sits inside the Font**, by a trap in raylib
;; rather than a choice here: MeasureTextEx returns (0,0) at once when
;; `texture.id` is 0. The hand-built font claims an id of 1 — there is no
;; texture — and if the texture landed anywhere else in the struct the id
;; would read 0 and every measurement below would collapse to zero.
;;
;; And what is NOT pinned, which matters as much: `glyph-padding` is read by
;; nothing raylib computes on the CPU, `offset-y` only moves a glyph when one
;; is actually drawn, and of each atlas rectangle only `width` is ever looked
;; at — `x`, `y` and `height` come back from get-glyph-atlas-rec exactly as
;; they were stored, which is the symmetric trap and proves nothing. Those
;; four rest on raylib's header and on sand.flan looking right. This is the
;; same limit raylib-ffi.flan records for Texture2D's width, height and
;; mipmaps, and it is written down for the same reason.
;;
;; The numbers are chosen so that no two are equal: base size 10, glyph count
;; 3, padding 2, advances 11 and 13, offsets 1, 2 and 3, atlas widths 5, 7
;; and 9. Everything prints exactly — no trigonometry, so unlike the rotated
;; camera in raylib-ffi.flan this can be compared as text.
;; The atlas rectangles, one per glyph. x and width differ per glyph so that
;; a fallback to the wrong index is a wrong number; height is the same 10 for
;; all three because raylib never reads it here.
(defonce glyph-recs [3 rl/Rectangle])
;; The glyphs themselves. `image` is raylib's own pixels for the glyph and is
;; left zeroed — it is present so the four ints in front of it are at the
;; right offsets and so a GlyphInfo is 40 bytes rather than 16. That claim is
;; checked: moving `image` to the front of the defstruct shifts the four ints
;; by 24 bytes, and the glyph search then finds nothing — every index reads 0
;; and glyph C answers with A's numbers.
(defonce glyphs [3 rl/GlyphInfo])
(defn build-glyphs [] ()
(set (at glyph-recs 0) (rl/Rectangle {.x 0.0 .y 0.0 .width 5.0 .height 10.0}))
(set (at glyph-recs 1) (rl/Rectangle {.x 5.0 .y 0.0 .width 7.0 .height 10.0}))
(set (at glyph-recs 2) (rl/Rectangle {.x 12.0 .y 0.0 .width 9.0 .height 10.0}))
;; 65 66 67 are A B C. Advances 11 and 13 for the first two; C's advance is
;; 0 on purpose, which is what sends raylib down the other branch.
(set (at glyphs 0) (rl/GlyphInfo {.value 65 .offset-x 1 .offset-y 0
.advance-x 11 .image (rl/Image {})}))
(set (at glyphs 1) (rl/GlyphInfo {.value 66 .offset-x 2 .offset-y 0
.advance-x 13 .image (rl/Image {})}))
(set (at glyphs 2) (rl/GlyphInfo {.value 67 .offset-x 3 .offset-y 0
.advance-x 0 .image (rl/Image {})})))
(defn show-bool [name string b bool] ()
(print name) (print " ")
(println (if b "yes" "no")))
(defn show-i [name string v i32] ()
(print name) (print " ") (print v) (println ""))
(defn show-v [name string v rl/Vector2] ()
(print name)
(print " ") (print (.x v))
(print " ") (print (.y v))
(println ""))
(defn show-rect [name string r rl/Rectangle] ()
(print name)
(print " ") (print (.x r))
(print " ") (print (.y r))
(print " ") (print (.width r))
(print " ") (print (.height r))
(println ""))
(defn main [] i32
(rl/set-trace-log-level :log-warning)
(build-glyphs)
;; The font. `texture` is a lie in every field but `id`, and the id is 1 for
;; the reason in the header note: at 0, raylib refuses to measure anything.
;;
;; It is built here rather than by a helper because a `defn` cannot say it
;; returns one: the parser decides whether the form after a parameter list
;; is a return type or the first body expression by looking the name up in
;; the set of types THIS FILE declares, and a package's types are not in it
;; — imports are resolved after parsing. `(defn the-font [] rl/Font ...)`
;; therefore parses rl/Font as an expression and fails with "unknown name".
;; That is a parser limit and not something this file wanted; it applies to
;; rl/Vector2 just as much as to rl/Font.
(let [f (rl/Font {.base-size 10 .glyph-count 3 .glyph-padding 2
.texture (rl/Texture2D {.id 1 .width 32 .height 16
.mipmaps 1 .format 7})
.recs (addr (at glyph-recs 0))
.glyphs (addr (at glyphs 0))})]
;; font-valid? reads the texture id and both array pointers, so it is a
;; null check on the three things everything below dereferences.
(show-bool "valid" (rl/font-valid? f))
;; ── The search ──────────────────────────────────────────────────────
;;
;; A linear walk of `glyph-count` entries comparing `value`. Three hits at
;; three different indices, and one miss: 'Z' is not in the font and
;; raylib answers index 0 rather than running off the end. Shrink
;; glyph-count — or land the field somewhere else — and 'C' misses too.
(show-i "index A" (rl/get-glyph-index f 65))
(show-i "index B" (rl/get-glyph-index f 66))
(show-i "index C" (rl/get-glyph-index f 67))
(show-i "index Z" (rl/get-glyph-index f 90))
;; ── Reading back through the two pointers ───────────────────────────
;;
;; The atlas rectangle for B is the second entry of `recs`, and the glyph
;; for C is the third of `glyphs`. Both are indexed by the search above,
;; so an exchange of the two pointers is a Rectangle read as a GlyphInfo.
(show-rect "atlas B" (rl/get-glyph-atlas-rec f 66))
(let [g (rl/get-glyph-info f 67)]
(show-i "glyph C value" (.value g))
(show-i "glyph C offset" (.offset-x g))
(show-i "glyph C advance" (.advance-x g)))
;; ── Measuring, which is where the arithmetic is ─────────────────────
;;
;; "AB" is 11 + 13 = 24 wide and base-size tall, at scale 1.
(show-v "measure AB" (rl/measure-text-ex f "AB" 10.0 0.0))
;; "ABC" adds C, whose advance is 0, so raylib falls back to the atlas
;; width plus the offset: 9 + 3 = 12, for 36. This line and the one above
;; disagree by exactly the amount that proves the fallback branch ran, and
;; that branch is the only thing in the file that reads Rectangle.width
;; out of the recs array — exchange x and width in the Rectangle defstruct
;; and this line reads 39 while the one above stays 24.
(show-v "measure ABC" (rl/measure-text-ex f "ABC" 10.0 0.0))
;; The same string at twice the size and a spacing of 3. Scale is 20/10,
;; so 24 doubles to 48, and spacing is added once per gap and not once per
;; glyph: 48 + 3 = 51. Exchange base-size and glyph-count and the scale
;; becomes 20/3 and the search bound becomes 10 — both wrong, in different
;; directions, on the same line.
(show-v "measure AB big" (rl/measure-text-ex f "AB" 20.0 3.0))
;; One glyph, so there is no gap for the spacing to land in: 11 at scale
;; 1, and the 3 does not appear. Without this line a spacing added per
;; glyph rather than per gap would pass everything above.
(show-v "measure A spaced" (rl/measure-text-ex f "A" 10.0 3.0)))
0)