flan/test/programs/raylib-font.flan
Joseph Ferano 9a820d86cd Sweep every field label from the colon spelling to the dot
The script is in tools/ rather than thrown away, because two lanes are
writing Flan in the old spelling right now and their files need the same
pass at merge.

It works on forms, not on text: a keyword becomes a dot only where it sits
in a field-label position inside a brace, so an enum member in value
position, a map key inside an EDN string and a type-position {K V} are all
left alone. :keys keeps its colon -- it names no field.
2026-09-12 14:47:54 +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.
(defvar 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.
(defvar 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 :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)