214 lines
9.6 KiB
Plaintext
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()
|