Closes the two open issues and lands the pending unreleased batch: - #90: `Sprite { atlas: 1 }` routes esys_sprite through atlas_draw_ex (scale/flip/tint), so cell / cell_span / strip ids of any size draw through the engine sprite-render system. examples/library/sprite_atlas is the pixel-readback regression. - #91: `become` from an @On(Event) listener / global handler / plain function no longer segfaults the compiler; it emits @L_scene_leave() (a dispatch on the live scene id) so the leaving scene's on-exit runs. UI_* handles are readable from any code (widget table built on first use). examples/library/scene_menus covers it. - fix: a windowed `ludicc -o` build that reaches the audio runtime only through the atlas/Assets preload import now links audio.ll + AVFoundation (the audio backend link was gated on a game-level Audio.* call, so any windowed game declaring Sprite failed to link). - the hand-written "Unreleased" CHANGELOG section is converted to changesets under changes/ so `x release` generates it. - plus the batch: engine-driven retained UI + UiClicked event, Overlay phase, TileSkin tilemap-render system, Key.* constants, Font/Ui/File namespaces, Sprite.strip, prefabs, managers, countdown fields, enum-typed machines, layer @Queries, ludic.prefs / ludic.dungeon packages, Ai.seek pathing, Solids.solid2, cursor confine (mode 3) fix, shooter centre-aim fix, reserved-word function diagnostic. Verified: x test (124/124), x test-tools, check-impl, check-vocabulary, check-docs, docs-gen + docs-check, bootstrap-cfree (seed is a fixpoint). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
292 lines
7.2 KiB
Text
292 lines
7.2 KiB
Text
# ui_draw.ludic — layout arrange, drawing, focus/activation and the game-facing
|
|
# accessors. Split out of ui.ludic (the storage/build/measure half).
|
|
|
|
# ---- arrange: rects, parents before children ------------------------------
|
|
function ui_arrange(i: int, x: int, y: int) -> void {
|
|
ui_rx[i] = x
|
|
ui_ry[i] = y
|
|
let t = ui_type[i]
|
|
if ui_is_container(t) == false { return }
|
|
let dir = ui_dir(t)
|
|
let pad = ui_pad[i]
|
|
let gap = ui_gap[i]
|
|
let innerw = ui_rw[i] - 2 * pad
|
|
let innerh = ui_rh[i] - 2 * pad
|
|
var total = 0
|
|
var ng = 0
|
|
var grows = 0
|
|
for c in 0 .. ui_n {
|
|
if ui_parent[c] == i {
|
|
if ui_visible[c] == 1 {
|
|
if dir == 1 { total = total + ui_rh[c] } else { total = total + ui_rw[c] }
|
|
ng = ng + 1
|
|
grows = grows + ui_grow[c]
|
|
}
|
|
}
|
|
}
|
|
if ng > 1 { total = total + gap * (ng - 1) }
|
|
var avail = innerw
|
|
if dir == 1 { avail = innerh }
|
|
let extra = max(avail - total, 0)
|
|
var cx = x + pad
|
|
var cy = y + pad
|
|
for c in 0 .. ui_n {
|
|
if ui_parent[c] == i {
|
|
if ui_visible[c] == 1 {
|
|
var kw = ui_rw[c]
|
|
var kh = ui_rh[c]
|
|
if ui_grow[c] != 0 {
|
|
if grows > 0 {
|
|
if dir == 1 { kh = kh + extra / grows } else { kw = kw + extra / grows }
|
|
}
|
|
}
|
|
let al = ui_eff_align(c, i)
|
|
ui_rw[c] = kw
|
|
ui_rh[c] = kh
|
|
if dir == 1 {
|
|
var ax = x + pad
|
|
if al == 1 { ax = x + pad + (innerw - kw) / 2 }
|
|
if al == 2 { ax = x + pad + (innerw - kw) }
|
|
ui_arrange(c, ax, cy)
|
|
cy = cy + kh + gap
|
|
} else {
|
|
var ay = y + pad
|
|
if al == 1 { ay = y + pad + (innerh - kh) / 2 }
|
|
if al == 2 { ay = y + pad + (innerh - kh) }
|
|
ui_arrange(c, cx, ay)
|
|
cx = cx + kw + gap
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function ui_layout() -> void {
|
|
if ui_active < 0 { return }
|
|
ui_measure()
|
|
var px = (rt_fbw - ui_rw[ui_active]) / 2
|
|
var py = (rt_fbh - ui_rh[ui_active]) / 2
|
|
if ui_haspos[ui_active] == 1 {
|
|
px = ui_x[ui_active]
|
|
py = ui_y[ui_active]
|
|
}
|
|
ui_arrange(ui_active, px, py)
|
|
}
|
|
|
|
# ---- drawing --------------------------------------------------------------
|
|
function ui_lighten(c: int) -> int {
|
|
var r = ((c >> 16) & 255)
|
|
var g = ((c >> 8) & 255)
|
|
var b = (c & 255)
|
|
r = r + (255 - r) * 4 / 10
|
|
g = g + (255 - g) * 4 / 10
|
|
b = b + (255 - b) * 4 / 10
|
|
return (((r << 16) | (g << 8)) | b)
|
|
}
|
|
|
|
function ui_draw_node(i: int) -> void {
|
|
if ui_visible[i] == 0 { return }
|
|
let t = ui_type[i]
|
|
let rx = ui_rx[i]
|
|
let ry = ui_ry[i]
|
|
let rw = ui_rw[i]
|
|
let rh = ui_rh[i]
|
|
let skin = ui_skin[i]
|
|
var focused = 0
|
|
if i == ui_focus { focused = 1 }
|
|
|
|
if t == WT_PANEL {
|
|
if skin >= 0 {
|
|
rt_draw_9slice(skin, rx, ry, rw, rh, ui_inset[i])
|
|
} else {
|
|
if ui_bg[i] >= 0 { rt_fill_rect(rx, ry, rw, rh, ui_bg[i]) }
|
|
if ui_border[i] >= 0 { rt_frame_rect(rx, ry, rw, rh, ui_border[i]) }
|
|
}
|
|
}
|
|
if t == WT_BUTTON {
|
|
var bg = ui_bg[i]
|
|
if bg < 0 { bg = 0x2a2a3a }
|
|
if focused == 1 { bg = ui_lighten(bg) }
|
|
if skin >= 0 {
|
|
rt_draw_9slice(skin, rx, ry, rw, rh, ui_inset[i])
|
|
} else {
|
|
rt_fill_rect(rx, ry, rw, rh, bg)
|
|
}
|
|
var bc = ui_border[i]
|
|
if bc < 0 { bc = 0x555577 }
|
|
if focused == 1 { bc = 0xffff00 }
|
|
rt_frame_rect(rx, ry, rw, rh, bc)
|
|
let s = ui_str(i)
|
|
var fg = ui_fg[i]
|
|
if fg < 0 { fg = 0xffffff }
|
|
let font = ui_font[i]
|
|
let size = ui_size[i]
|
|
ui_draw_text(font, rx + (rw - ui_tw(font, s, size)) / 2, ry + (rh - ui_th(font, size)) / 2, s, fg, size)
|
|
}
|
|
if t == WT_LABEL {
|
|
let s = ui_str(i)
|
|
var fg = ui_fg[i]
|
|
if fg < 0 { fg = 0xffffff }
|
|
let font = ui_font[i]
|
|
let size = ui_size[i]
|
|
let pad = ui_pad[i]
|
|
let tw = ui_tw(font, s, size)
|
|
var tx = rx + pad
|
|
let al = ui_align[i]
|
|
if al == 1 { tx = rx + (rw - tw) / 2 }
|
|
if al == 2 { tx = rx + rw - pad - tw }
|
|
ui_draw_text(font, tx, ry + pad, s, fg, size)
|
|
}
|
|
if t == WT_IMAGE {
|
|
if ui_img[i] >= 0 { rt_draw_image_scaled(ui_img[i], rx, ry, rw, rh) }
|
|
}
|
|
for c in 0 .. ui_n {
|
|
if ui_parent[c] == i { ui_draw_node(c) }
|
|
}
|
|
}
|
|
|
|
# ---- focus and activation -------------------------------------------------
|
|
var ui_fl: words = null
|
|
|
|
function ui_focusables() -> int {
|
|
if (ui_fl == null) { ui_fl = words(UI_MAX) }
|
|
var n = 0
|
|
for i in 0 .. ui_n {
|
|
if ui_focusable[i] == 1 {
|
|
if ui_visible[i] == 1 {
|
|
if ui_under(i, ui_active) {
|
|
ui_fl[n] = i
|
|
n = n + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
# a focused control was activated (Enter / Space / pad A): the event-driven form of
|
|
# ui_clicked — `@On(UiClicked) handler { if id == UI_Play { ... } }`.
|
|
event UiClicked { id: int = 0 }
|
|
|
|
# Ui.close(): no menu is active (the same as opening root -1)
|
|
function rt_ui_close() -> void { rt_ui_open(0 - 1) }
|
|
|
|
function rt_ui_open(root: int) -> void {
|
|
ui_active = root
|
|
let n = ui_focusables()
|
|
ui_focus = 0 - 1
|
|
if n > 0 { ui_focus = ui_fl[0] }
|
|
}
|
|
|
|
function rt_ui_tick(k: int) -> void {
|
|
for i in 0 .. ui_n { ui_fired[i] = 0 } # a closed UI clears clicks too, so a stale
|
|
if ui_active < 0 { return } # activation never leaks to the next frame/scene
|
|
ui_layout()
|
|
let n = ui_focusables()
|
|
if n == 0 {
|
|
ui_focus = 0 - 1
|
|
return
|
|
}
|
|
var cur = 0 - 1
|
|
for i in 0 .. n {
|
|
if ui_fl[i] == ui_focus { cur = i }
|
|
}
|
|
if cur < 0 {
|
|
cur = 0
|
|
ui_focus = ui_fl[0]
|
|
}
|
|
if k == 119 { # 'w'
|
|
cur = (cur - 1 + n) % n
|
|
ui_focus = ui_fl[cur]
|
|
}
|
|
if k == 115 { # 's'
|
|
cur = (cur + 1) % n
|
|
ui_focus = ui_fl[cur]
|
|
}
|
|
var fire = 0
|
|
if k == 32 { fire = 1 }
|
|
if k == 10 { fire = 1 }
|
|
if k == 13 { fire = 1 }
|
|
if fire == 1 { ui_fired[ui_focus] = 1; emit UiClicked(id: ui_focus) }
|
|
}
|
|
|
|
function rt_ui_render() -> void {
|
|
if ui_active < 0 { return }
|
|
ui_layout()
|
|
ui_draw_node(ui_active)
|
|
}
|
|
|
|
# ---- game-facing accessors ------------------------------------------------
|
|
function rt_ui_clicked(id: int) -> bool {
|
|
if id < 0 { return false }
|
|
if id >= ui_n { return false }
|
|
return ui_fired[id] == 1
|
|
}
|
|
|
|
function rt_ui_set_text(id: int, s: string) -> void {
|
|
if id < 0 { return }
|
|
if id >= ui_n { return }
|
|
let base = id * 96
|
|
var i = 0
|
|
var ch = s[0]
|
|
while ch != 0 {
|
|
if i >= 95 { ch = 0 }
|
|
if ch != 0 {
|
|
ui_dyn[base + i] = ch
|
|
i = i + 1
|
|
ch = s[i]
|
|
}
|
|
}
|
|
ui_dyn[base + i] = 0
|
|
ui_hasdyn[id] = 1
|
|
}
|
|
|
|
function rt_ui_set_int(id: int, n: int) -> void {
|
|
if id < 0 { return }
|
|
if id >= ui_n { return }
|
|
let base = id * 96
|
|
var at = 0
|
|
var v = n
|
|
if v < 0 {
|
|
ui_dyn[base] = 45
|
|
at = 1
|
|
v = 0 - v
|
|
}
|
|
if v == 0 {
|
|
ui_dyn[base + at] = 48
|
|
at = at + 1
|
|
} else {
|
|
var digits = 0
|
|
var t = v
|
|
while t > 0 {
|
|
digits = digits + 1
|
|
t = t / 10
|
|
}
|
|
var p = digits
|
|
while p > 0 {
|
|
var div = 1
|
|
for k in 1 .. p { div = div * 10 }
|
|
ui_dyn[base + at] = 48 + (v / div) % 10
|
|
at = at + 1
|
|
p = p - 1
|
|
}
|
|
}
|
|
ui_dyn[base + at] = 0
|
|
ui_hasdyn[id] = 1
|
|
}
|
|
|
|
function rt_ui_focus(id: int) -> void {
|
|
if id < 0 { return }
|
|
if id >= ui_n { return }
|
|
ui_focus = id
|
|
}
|
|
|
|
function rt_ui_focused() -> int {
|
|
return ui_focus
|
|
}
|
|
|
|
function rt_ui_visible(id: int, v: int) -> void {
|
|
if id < 0 { return }
|
|
if id >= ui_n { return }
|
|
ui_visible[id] = v
|
|
}
|