feat(ui): ui_scale and ui_box, on-submit, zoom (and a length over a length in calc), on-hold

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 03:31:38 +03:00
parent 3d2a103dfb
commit ec9a650e65
24 changed files with 323 additions and 23 deletions

View file

@ -0,0 +1,19 @@
# ui_box.ludic — what a host reads from outside the interface: ui_scale(), the scale it is drawn at
# (the renderer's), and ui_box("id"), where an element was laid out when its screen was last shown,
# in screen pixels (null for one that is not there) - to frame a 3D view beside a panel.
import "ludic.ui"
program UiBox {
numbers float
const PAGE: string = "<ui><style>.side { width: 100px; height: 60px; margin: 10px }</style><screen name='b'><p>title</p><div id='panel' class='side'/></screen></ui>"
function scale() -> float { return 1.5 }
entry {
let b = new UiBackend
b.scale = fn scale
ui_backend(b)
ui_load_text(PAGE, "b.xml")
ui_show("b", null, 20.0, 30.0, 800.0, 600.0)
let p = ui_box("panel")
let none = ui_box("nothing")
print(`scale {ui_scale()} | panel {int(p.x)},{int(p.y)} {int(p.w)}x{int(p.h)} | missing {none == null}`)
}
}

View file

@ -0,0 +1,59 @@
# ui_hold.ludic — on-hold fires every frame a button is held, by the pointer (still when it is dragged
# off, until let go) or by Enter or the pad's A on the focus, with event.dt and event.t on the ui clock,
# the same at 60 and 240 Hz; on-down and on-up still mark the ends.
import "ludic.ui"
program UiHold {
numbers float
var now: float = 0.0
var held: float = 0.0
var frames: int = 0
var last_t: float = 0.0
var ends: string = ""
view Craft {
on tick(dt: float, t: float) {
held = held + dt
frames = frames + 1
last_t = t
}
on down() { ends = ends + "d" }
on up() { ends = ends + "u" }
}
const PAGE: string = "<ui><screen name='h'><button on-hold='tick(event.dt, event.t)' on-down='down()' on-up='up()'>Make</button></screen></ui>"
function clock() -> float { return now }
function frame(i: UiInput) -> UiNode {
ui_input(i)
ui_show("h", view_craft(), 0.0, 0.0, 400.0, 400.0)
let r: UiNode = ui_nodes("h", view_craft())
ui_place(r, 0.0, 0.0, 400.0, 400.0)
return r.children[0]
}
# held for `secs` at `hz`, by the pointer (off the button after the first frame) or by A
function hold(b: UiNode, secs: float, hz: int, pad: bool) -> string {
held = 0.0
frames = 0
let n = int(secs * float(hz) + 0.5)
for k in 0 .. n + 1 {
let i = new UiInput
i.pad_a = pad
i.down = not pad
i.x = b.x + 5.0
i.y = b.y + 5.0
if k > 0 and not pad { i.x = 390.0 }
frame(i)
now = now + 1.0 / float(hz)
}
frame(new UiInput)
now = now + 1.0 / float(hz)
return `{frames} frames {int(Math.round(held * 1000.0))} ms t {int(Math.round(last_t * 1000.0))}`
}
entry {
ui_backend(new UiBackend)
ui_clock(fn clock)
ui_load_text(PAGE, "h.xml")
let b = frame(new UiInput)
let slow = hold(b, 0.5, 60, false)
let fast = hold(b, 0.5, 240, false)
let pad = hold(b, 0.25, 60, true)
print(`{slow} | {fast} | pad {pad} | {ends}`)
}
}

View file

@ -0,0 +1,45 @@
# ui_submit.ludic — on-submit on a text field: Enter (or the pad's A) while it has the focus sends what
# it holds with event.value; the focus stays, and so does the text, unless clear-on-submit empties it.
import "ludic.ui"
program UiSubmit {
numbers float
var said: string = ""
var chat: string = ""
var name: string = ""
view Chat {
chat = chat
name = name
on set_chat(v: string) { chat = v }
on set_name(v: string) { name = v }
on send(v: string) { said = said + "[" + v + "]" }
}
const PAGE: string = "<ui><screen name='c'><input type='text' label='Say' value='{chat}' clear-on-submit on-change='set_chat(event.value)' on-submit='send(event.value)'/><input type='text' label='Name' value='{name}' on-change='set_name(event.value)' on-submit='send(event.value)'/></screen></ui>"
function frame(i: UiInput) -> void {
ui_input(i)
ui_show("c", view_chat(), 0.0, 0.0, 400.0, 400.0)
}
function press(tab: bool, enter: bool, pad: bool, typed: string) -> void {
let i = new UiInput
i.tab = tab
i.enter = enter
i.pad_a = pad
i.typed = typed
frame(i)
frame(new UiInput)
}
entry {
ui_backend(new UiBackend)
ui_load_text(PAGE, "c.xml")
frame(new UiInput)
press(true, false, false, "")
press(false, false, false, "hi")
press(false, true, false, "")
let after = `{said} chat '{chat}'`
press(false, false, false, "yo")
press(false, false, true, "")
press(true, false, false, "")
press(false, false, false, "Ada")
press(false, true, false, "")
print(`{after} | {said} chat '{chat}' name '{name}'`)
}
}

View file

@ -0,0 +1,21 @@
# ui_zoom.ludic — zoom scales an element and everything inside it: its lengths, its text and its
# layout, and zooms multiply; zoom takes calc() and min(), with a length over a length a plain number,
# so a HUD can shrink to a narrow window (zoom: min(1, calc(100vw / 800px))).
import "ludic.ui"
program UiZoom {
numbers float
const STYLE: string = "<style>.box { width: 100px; padding: 10px }\n.half { zoom: 0.5 }\n.twice { zoom: 200% }\n.fit { zoom: min(1, calc(100vw / 800px)) }\np { height: 20px }\n.em { width: 2em }</style>"
const BODY: string = "<screen name='z'><div class='box'><p>a</p></div><div class='box half'><p>b</p><div class='twice'><div class='box'/></div></div><div class='fit'><div class='box'/><div class='em'/></div></screen>"
entry {
ui_backend(new UiBackend)
ui_load_text("<ui>" + STYLE + BODY + "</ui>", "z.xml")
ui_show("z", null, 0.0, 0.0, 400.0, 600.0)
let r: UiNode = ui_nodes("z", null)
ui_place(r, 0.0, 0.0, 400.0, 600.0)
let plain = r.children[0]
let half = r.children[1]
let inner = half.children[1].children[0]
let fit = r.children[2]
print(`plain {int(plain.cw)} pad {int(plain.pt)} text {int(plain.children[0].rsize)} h {int(plain.children[0].ch)} | half {int(half.cw)} pad {int(half.pt)} text {int(half.children[0].rsize)} h {int(half.children[0].ch)} | half of twice {int(inner.cw)} | fit {int(fit.children[0].cw)} em {int(fit.children[1].cw)}`)
}
}