flan/examples/text-rectangle-bounds.fln

214 lines
9.6 KiB
Plaintext

;;;; raylib [text] example - Rectangle bounds
;;;;
;;;; examples/text/text_rectangle_bounds.c. This is the example that could not
;;;; be ported at all until `slice-from` existed, and it is the reason the
;;;; form was built rather than an application found for it afterwards.
;;;;
;;;; Its inner loop reads `font.recs[index]` and `font.glyphs[index]`. Both
;;;; fields are `Ptr(T)` — that is what raylib hands back and there is nothing
;;;; else it could hand back — and a pointer from C carries no length, so
;;;; `font.recs[i]` answered `Ptr(rl/Rectangle) cannot be indexed` and
;;;; the whole example stopped there. Element 0 through `deref` was the entire
;;;; readable surface of a two-hundred-glyph array.
;;;;
;;;; The count is not missing; it is in the struct, one field over, as
;;;; `glyph-count`. What was missing was a way to say so. `slice-from` is
;;;; that way, and `rl/font-recs` and `rl/font-glyphs` in the bindings are
;;;; where this example says it — once, beside the invariant, rather than at
;;;; every call site. Note which shape this is: the count is a sibling *field*,
;;;; not an out-parameter, so a per-binding declaration naming a count argument
;;;; (the alternative weighed at the time) could not have reached it.
;;;;
;;;; This is also the only *procedural* font example upstream — every other one
;;;; in examples/text needs a TTF on disk — so until now the hand-written `Font`
;;;; surface (`get-font-default`, `get-glyph-index`, `draw-text-codepoint`, the
;;;; `GlyphInfo` and `Font` structs) had no example that called it.
;;;;
;;;; Two departures from the C, both deliberate:
;;;;
;;;; - `DrawTextBoxedSelectable` is not ported. The C defines it and then calls
;;;; it from `DrawTextBoxed` with `selectStart = 0`, `selectLength = 0` and
;;;; two `WHITE`s, so the selection half is dead code in this example: no
;;;; glyph is ever selected and no selection background is ever drawn. With
;;;; it goes the `k`/`lastk` pair, which exists only to keep a *codepoint*
;;;; index for the selection while `i` walks bytes. What is left is the word
;;;; wrap, which is the thing the example is about.
;;;;
;;;; - `GetCodepoint` wants a pointer into the middle of the text, and Flan has
;;;; the operation the C is spelling by hand: `string(slice(b, i, n))` is the
;;;; tail from `i` with no copy and no pointer arithmetic. It is not free:
;;;; the shim NUL-terminates a copy on the way into C, into a 256-byte stack
;;;; buffer or a malloc when the tail is longer — and this message is 284
;;;; bytes, so the first thirty glyphs of each pass do allocate. Fine for one
;;;; string drawn once a frame; worth knowing before reaching for the same
;;;; shape in a hot loop.
import rl "vendor:raylib"
const screen-width = 800
const screen-height = 450
const message = "Text cannot escape\tthis container\t...word wrap also works when active so here's a long text for testing.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nec ullamcorper sit amet risus nullam eget felis eget."
;; The two halves of the wrap state machine. wordWrap on starts in `measure`,
;; which finds where the line has to break and then replays the same bytes in
;; `draw`; wordWrap off never leaves `draw`.
const measure = 0
const draw = 1
;; Draw `text` inside `rec`, breaking lines on words when `is-word-wrap`.
;;
;; The two `slice-from` uses are inside rl/font-recs and rl/font-glyphs;
;; from here they are ordinary slices, bounds-checked like any other, and the
;; index comes from raylib's own get-glyph-index so it is in range by
;; construction.
fn draw-text-boxed(font: rl/Font, text: [const u8], rec: rl/Rectangle, font-size: f32, spacing: f32, is-word-wrap: bool, tint: rl/Color) -> ()
let glyphs = rl/font-glyphs(font)
recs = rl/font-recs(font)
length = i32(length(text))
scale = font-size / f32(font.base-size)
let line-h = f32(font.base-size + font.base-size / 2) * scale
let off-x = f32(0.0)
off-y = f32(0.0)
state = if is-word-wrap then measure else draw
;; Byte offsets: where the current line begins and where it must end.
;; -1 is "not decided yet", which is why end-line is compared against 1
;; rather than 0 below — the C's `(endLine < 1)`.
start-line = -1
end-line = -1
i = 0
while i < length
let size = 0
let cp = rl/get-codepoint(str(slice(text, i, length)), addr(size))
;; A byte that decodes to nothing answers '?' and raylib would normally
;; stop; this walks on one byte at a time so the bad bytes get drawn.
if cp == i32(\?)
size = 1
i += size - 1
let gi = rl/get-glyph-index(font, cp)
w = f32(0.0)
if cp != i32(\newline)
;; advance-x is how far the pen moves; 0 means "use the atlas
;; rectangle's width instead". This is the line that needed the
;; slices — two parallel arrays, indexed by the same glyph index.
w =
if glyphs[gi].advance-x == 0
recs[gi].width * scale
else
f32(glyphs[gi].advance-x) * scale
;; Spacing is added per gap, not per glyph, so the last one on the
;; line does not get it.
if i + 1 < length
w += spacing
if state == measure
;; Any of these three is a legal place to break.
if cp == i32(\space) or cp == i32(\tab) or cp == i32(\newline)
end-line = i
if off-x + w > rec.width
if end-line < 1
end-line = i
if i == end-line
end-line -= size
if start-line + size == end-line
end-line = i - size
state = draw
elif i + 1 == length
end-line = i
state = draw
elif cp == i32(\newline)
state = draw
;; Having decided where the line ends, rewind to where it began
;; and walk the same bytes again, drawing this time.
if state == draw
off-x = 0.0
i = start-line
w = 0.0
else
if cp == i32(\newline)
if not is-word-wrap
off-y += line-h
off-x = 0.0
else
if not is-word-wrap and off-x + w > rec.width
off-y += line-h
off-x = 0.0
;; Out of vertical room: stop, rather than drawing outside.
if off-y + f32(font.base-size) * scale > rec.height
break
if cp != i32(\space) and cp != i32(\tab)
rl/draw-text-codepoint(font, cp,
rl/Vector2{.x rec.x + off-x, .y rec.y + off-y},
font-size, tint)
if is-word-wrap and i == end-line
off-y += line-h
off-x = 0.0
start-line = end-line
end-line = -1
w = 0.0
state = measure
;; Leading spaces do not push the pen along; everything else does.
if off-x != 0.0 or cp != i32(\space)
off-x += w
i += 1
fn main() -> ()
rl/init-window(screen-width, screen-height,
"raylib [text] example - draw text inside a rectangle")
defer rl/close-window()
let text = bytes-view(message)
is-resizing = false
is-word-wrap = true
let container = rl/Rectangle{.x 25.0, .y 25.0, .width f32(screen-width) - 50.0, .height f32(screen-height) - 250.0}
let resizer = rl/Rectangle{.x 0.0 .y 0.0 .width 14.0 .height 14.0}
let min-width = f32(60.0)
min-height = f32(60.0)
max-width = f32(screen-width) - 50.0
max-height = f32(screen-height) - 160.0
last-mouse = rl/Vector2{.x 0.0 .y 0.0}
border = rl/maroon
;; Needs the window: the default font is loaded as part of init-window.
font = rl/get-font-default()
rl/set-target-fps(60)
until rl/window-should-close()
if rl/is-key-pressed(:key-space)
is-word-wrap = not is-word-wrap
let mouse = rl/get-mouse-position()
;; The border fades while the pointer is over the container.
if rl/check-collision-point-rec(mouse, container)
border = rl/fade(rl/maroon, 0.4)
elif not is-resizing
border = rl/maroon
if is-resizing
if rl/is-mouse-button-released(:mouse-left)
is-resizing = false
let w = container.width + (mouse.x - last-mouse.x)
let h = container.height + (mouse.y - last-mouse.y)
container.width = clamp(w, min-width, max-width)
container.height = clamp(h, min-height, max-height)
elif rl/is-mouse-button-down(:mouse-left) and rl/check-collision-point-rec(mouse, resizer)
is-resizing = true
resizer.x = container.x + container.width - 17.0
resizer.y = container.y + container.height - 17.0
last-mouse = mouse
rl/begin-drawing()
rl/clear-background(rl/raywhite)
rl/draw-rectangle-lines-ex(container, 3.0, border)
;; The container with a little padding, which is where the wrap runs.
draw-text-boxed(font, text,
rl/Rectangle{.x container.x + 4.0, .y container.y + 4.0, .width container.width - 4.0, .height container.height - 4.0},
20.0, 2.0, is-word-wrap, rl/gray)
rl/draw-rectangle-rec(resizer, border)
rl/draw-rectangle(0, screen-height - 54, screen-width, 54, rl/gray)
rl/draw-rectangle-rec(rl/Rectangle{.x 382.0, .y f32(screen-height) - 34.0, .width 12.0, .height 12.0},
rl/maroon)
rl/draw-text("Word Wrap: ", 313, screen-height - 115, 20, rl/black)
if is-word-wrap
rl/draw-text("ON", 447, screen-height - 115, 20, rl/red)
else
rl/draw-text("OFF", 447, screen-height - 115, 20, rl/black)
rl/draw-text("Press [SPACE] to toggle word wrap", 218, screen-height - 86,
20, rl/gray)
rl/draw-text("Click hold & drag the to resize the container", 155,
screen-height - 38, 20, rl/raywhite)
rl/end-drawing()