;;;; Drawing a number, now that the language can make one into a string. ;;;; ;;;; This file used to be the workaround for a gap: five of the ten ported ;;;; examples call raylib's `TextFormat` to put a number on the screen, and ;;;; nothing in Flan could reach `draw-text` with one — `i64->bytes` answered a ;;;; `[u8]`, `draw-text` wanted a `string`, and there was no bridge and no ;;;; allocator to build one in. So a number was drawn one glyph at a time out ;;;; of a `[10 string]` table. ;;;; ;;;; `(string b)` closed that. It is the mirror of `(bytes s)` and costs no ;;;; instructions — a `string` and a `[u8]` are the same 16-byte %slice — so ;;;; `(string (i64->bytes n))` draws in one call and the table, the per-glyph ;;;; pen and the digit arithmetic behind them are gone. ;;;; ;;;; What is left is the part `(string ...)` does not answer, which is ;;;; *formatting*: `i64->bytes` has no field width, so "%03i" still has to be ;;;; assembled, and `f64->bytes` is `%g` and not "%.02f", so a fixed number of ;;;; decimal places still has to be split and drawn in two pieces. Those two ;;;; are why the file survives at all, and the three signatures are unchanged ;;;; so the five callers did not have to move. ;;;; ;;;; `TextFormat` itself is still not bindable, and not because of the FFI ;;;; rules: it is variadic, so its signature is not a signature, and it returns ;;;; a `char *` into a rotating static buffer, which `declare-c` refuses by ;;;; name anyway. ;;;; ;;;; ONE TRAP, and it is the reason every function below is written as a strict ;;;; sequence of format-draw-measure rather than as a let of several pieces: ;;;; `i64->bytes` and `f64->bytes` both write into a single shared static ;;;; buffer in the runtime, overwritten by the next such call. `(string ...)` ;;;; does not copy it. So a number must be *drawn before the next one is ;;;; formatted* — holding two at once is wrong pixels with no crash and no ;;;; diagnostic. ;;;; ;;;; 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 piece on top of the first. (import rl "vendor:raylib") ;; Padding is drawn from a literal, one zero at a time. This is the last of the ;; old glyph table and it is here only because there is no field width. (defconst zero-glyph "0") ;; One piece of text, 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. ;; ;; Nothing may be formatted between the draw and the measure: `s` may be a view ;; of the shared buffer, and both calls have to see the same bytes. (defn draw-piece [s string x i32 y i32 size i32 color rl/Color] i32 (rl/draw-text s x y size color) (rl/measure-text s size)) ;; How many decimal digits `n` has, for n >= 0. 0 has one. Only the padded ;; forms need it now — it is how many zeroes go in front. (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 number, in one draw-text. 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. ;; ;; The sign comes free now: i64->bytes renders "-7" itself, which also retires ;; the old note about i32's most negative value — it is widened to i64 before ;; formatting, so there is no negation to wrap. (defn draw-int [n i32 x i32 y i32 size i32 color rl/Color] i32 (draw-piece (string (i64->bytes (i64 n))) x y size color)) ;; The same with a fixed minimum number of digits — the C's "%03i". A number ;; wider than `width` is drawn in full rather than truncated, which is what ;; printf does too. ;; ;; The zeroes are drawn first and the number after, so the one formatted value ;; is still live when it is drawn. A sign goes in front of the padding, as ;; printf's "%03i" does for -7 → "-07"; hence the magnitude is what gets ;; counted and the minus is drawn separately. (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-piece "-" cx y size color))) (set v (- 0 v))) (let [pad (- width (digit-count v))] (dotimes [i pad] (set cx (+ cx (draw-piece zero-glyph cx y size color))))) (set cx (+ cx (draw-int v 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. ;; ;; `f64->bytes` is not used at all — it is "%g", which would print 0.5 for a ;; value asked for at three places and 1e+06 for a large one. The split into ;; two integers is what buys the fixed width, and it also keeps every formatted ;; value drawn before the next one is made. (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-piece "-" 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-piece "." cx y size color))) (set cx (+ cx (draw-int-padded frac places cx y size color))))) (- cx x)))