ludic/tools/ui-preview/font.ludic
Orkuncakilkaya 49b5e80af5 ludic ui-preview: ludic.ui components previewed for a studio over stdio (R5)
A prebuilt tool, bin/ludic-ui-preview (built by ludic-dev build, shipped beside ludic-lsp, run as
`ludic ui-preview [--font DIR]`): ludic.ui with a backend that records every draw call as a line,
and mock component classes the studio describes (load / model / calls). frame t runs ui_show at
interface time t; text is measured on the CPU with the game's font.json metrics; locale goes
through ludic.i18n; tree / box / rules inspect the frame. No game code, no GPU, no network. The wire
protocol is frozen as tools/ui-preview/protocol-v1.md; smoke.txt is a transcript to run.

ludic.ui gains, all additive: UiClass.make_of, UiBackend.said / emitted, UiRule.text / at (with
comment line breaks kept so rule lines count true, and a class's styles read under its .lss path),
and ui_root, ui_find, ui_rules_of, ui_rule_value, ui_building, ui_class, ui_class_load,
ui_file_forget, ui_errors_clear; ui_nine_cuts_into exported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:53:26 +03:00

80 lines
2.9 KiB
Text

# font.ludic - text measured as ludic.render3d's overlay measures it (overlay_font, ov_text_w), from
# the same font.json and with no GPU: a glyph's advance times the size in whole pixels, summed, and
# the sum cut to a whole pixel. With no font, a character is half the size wide.
const PV_MAP_N: int = 8192
@alloc_ok("a font read: once per font line")
function pv_font_load(pv: mut PvState, dir: string) -> bool {
let meta = Fs.read_text(dir + "/font.json")
if meta == null { return false }
let j = Json.parse(meta)
if Value.kind(j) != 6 or Value.has(j, "adv") == 0 { return false }
let adv = Value.get(j, "adv")
let n = Value.count(adv)
var first = 32
if Value.has(j, "first") != 0 { first = Value.as_int(Value.get(j, "first")) }
let codes = Value.get(j, "codes")
pv.fn_adv = new []float
pv.fn_map = new []int
pv.fn_hi_cp = new []int
pv.fn_hi_g = new []int
for i in 0 .. n { push(pv.fn_adv, Value.as_float(Value.at(adv, i))) }
for c in 0 .. PV_MAP_N { push(pv.fn_map, 0) }
for i in 0 .. n {
var cp = first + i
if Value.kind(codes) == 5 and i < Value.count(codes) { cp = Value.as_int(Value.at(codes, i)) }
if cp >= 0 and cp < PV_MAP_N { pv.fn_map[cp] = i + 1 } else if cp >= PV_MAP_N {
push(pv.fn_hi_cp, cp)
push(pv.fn_hi_g, i)
}
}
pv.fn_space = 0
if pv.fn_map[32] > 0 { pv.fn_space = pv.fn_map[32] - 1 }
pv.fn_qmark = pv.fn_space
if pv.fn_map[63] > 0 { pv.fn_qmark = pv.fn_map[63] - 1 }
pv.font_dir = dir
pv.fn_on = true
return true
}
# the glyph for a code point, as ov_glyph: a control character is a space, a missing one is '?'
function pv_glyph(pv: PvState, cp: int) -> int {
if cp < 32 { return pv.fn_space }
if cp < PV_MAP_N {
let g = pv.fn_map[cp]
if g > 0 { return g - 1 }
return pv.fn_qmark
}
for i in 0 .. len(pv.fn_hi_cp) {
if pv.fn_hi_cp[i] == cp { return pv.fn_hi_g[i] }
}
return pv.fn_qmark
}
# the bytes the UTF-8 sequence at s[i] takes, as ov_u8 reads it (a broken one is one byte)
function pv_u8_len(s: string, i: int) -> int {
let n = len(s)
let b0 = s[i] & 255
if b0 >= 240 and b0 < 248 and i + 3 < n { return 4 }
if b0 >= 224 and b0 < 240 and i + 2 < n { return 3 }
if b0 >= 192 and b0 < 224 and i + 1 < n { return 2 }
return 1
}
function pv_u8_cp(s: string, i: int, k: int) -> int {
let b0 = s[i] & 255
if k == 4 { return ((b0 & 7) << 18) | ((s[i + 1] & 63) << 12) | ((s[i + 2] & 63) << 6) | (s[i + 3] & 63) }
if k == 3 { return ((b0 & 15) << 12) | ((s[i + 1] & 63) << 6) | (s[i + 2] & 63) }
if k == 2 { return ((b0 & 31) << 6) | (s[i + 1] & 63) }
if b0 >= 128 { return 63 }
return b0
}
# the width of s at `size` pixels a em, in whole pixels
function pv_measure(pv: PvState, s: string, size: float) -> float {
let px = float(int(size))
var w = 0.0
var i = 0
let n = len(s)
while i < n {
let k = pv_u8_len(s, i)
if pv.fn_on { w = w + pv.fn_adv[pv_glyph(pv, pv_u8_cp(s, i, k))] * px } else { w = w + 0.5 * px }
i += k
}
return float(int(w))
}