ludic/packages/ludic.ui/render3d_image.ludic
Orkuncakilkaya 9f59000f5b feat(ui): number and key fields, notes, anchored popovers, text-shadow, and pictures as painted
- <input type="number" min max step>: typed digits replace its value (a minus when min allows one,
  a point when step has a fraction), Enter or leaving the field commits them held between min and
  max, the left and right arrows step it.
- <input type="key">: Enter or a click starts it listening and the next key is its value, Tab, the
  arrows and Enter included; Esc stops it listening, Backspace clears it, `shown` names the value,
  and ui_capturing() tells the host to leave its keys alone (through the frame that ended it).
- note="..." under any control's label (.ui-labels > .ui-label + .ui-note); a range's (or number's)
  value with decimals, format="percent" and a unit.
- A popover with `anchor="id"` (the nearest element of that id) or a bare `anchor` (the element
  before it) sits beside it by `placement` (right, left, bottom, top), its margin the gap, opens to
  the other side where it would leave the screen, and is kept on it.
- A tooltip's title splits into lines at a newline or a written \n.
- text-shadow, inherited, drawn sharp under the text.
- ui_opacity(): the group opacity a native's draw is at.
- border-image is drawn as painted with no background colour, tinted by one, and not at all when
  that colour is transparent (it drew a white nine-slice).
- A picture file is drawn untinted, as a browser draws one; an atlas cell (prefix:name) still takes
  the color around it, and an <img> with its own color is tinted by it.
- The render3d backend loads a picture again when its file changes or appears (a failed load was
  kept for ever), and at once after ui_image_reload(path); takes a path with a drive letter (C:/...)
  as a path rather than an atlas; and slices a nine-slice by its texture's own width and height.
- ui_inputs.ludic and ui_look.ludic; LANGUAGE.md and the changeset say all of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:55:20 +03:00

83 lines
3.2 KiB
Text

# render3d_image.ludic - pictures for the render3d backend: a texture by path (its sRGB bytes as they
# are, clamped at its edges, loaded again when the file changes) and a cell of a named atlas
module ludic_ui
function r3u_tex(path: string) -> int {
for i in 0 .. len(r3u_tex_paths) {
if r3u_tex_paths[i] == path { return r3u_tex_fresh(i) }
}
push(r3u_tex_paths, path)
push(r3u_tex_ids, r3u_tex_load(path))
push(r3u_tex_sizes, Fs.size(path))
push(r3u_tex_seen, r3u_now())
return r3u_tex_ids[len(r3u_tex_ids) - 1]
}
# a picture is looked at again every half second: a file that has changed (or that was not there,
# and is now) is loaded again, so an <img> of a photograph written over the old one shows the new one
function r3u_tex_fresh(i: int) -> int {
let now = r3u_now()
if now - r3u_tex_seen[i] < 0.5 and now >= r3u_tex_seen[i] { return r3u_tex_ids[i] }
r3u_tex_seen[i] = now
let size = Fs.size(r3u_tex_paths[i])
if size == r3u_tex_sizes[i] and (r3u_tex_ids[i] != 0 or size < 0) { return r3u_tex_ids[i] }
if r3u_tex_ids[i] != 0 { gpu_tex_free(r3u_tex_ids[i]) }
r3u_tex_ids[i] = r3u_tex_load(r3u_tex_paths[i])
r3u_tex_sizes[i] = size
return r3u_tex_ids[i]
}
# the interface is drawn in sRGB, and a picture's bytes are sRGB, so they are taken as they are
# (not decoded to linear, which would darken it); clamped at its edges
function r3u_tex_load(path: string) -> int {
let t = tex_load(path, false)
if t != 0 {
gpu_tex_bind(GPU_TEX2D, t)
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
}
return t
}
# a picture the program has written again, loaded again the next time it is drawn
export function ui_image_reload(path: string) -> void {
for i in 0 .. len(r3u_tex_paths) {
if r3u_tex_paths[i] == path {
r3u_tex_seen[i] = -1000000.0
r3u_tex_sizes[i] = -2
}
}
}
function r3u_image(src: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void {
var colon = -1
for i in 0 .. len(src) {
if colon < 0 and src[i] == 58 { colon = i }
}
if colon > 0 {
let prefix: string = src[0..colon]
for k in 0 .. len(r3u_atlas) {
if r3u_atlas[k] == prefix {
r3u_cell(k, src[colon + 1..len(src)], x, y, w, h, c, a)
return
}
}
}
let t = r3u_tex(src)
if t != 0 { ov_sub(t, int(x), int(y), int(w), int(h), 0.0, 0.0, 1.0, 1.0, r3u_r(c), r3u_g(c), r3u_b(c), a) }
}
function r3u_cell(k: int, name: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void {
let names = r3u_atlas_names[k]
var idx = -1
for i in 0 .. len(names) {
if names[i] == name { idx = i }
}
if idx < 0 and len(name) > 0 and name[0] >= 48 and name[0] <= 57 { idx = int(el_num(name)) }
if idx < 0 {
ui_err(`{r3u_atlas[k]}:{name}: the atlas has no such picture`)
return
}
let cols = r3u_atlas_cols[k]
let rows = r3u_atlas_rows[k]
let cx = idx % cols
let cy = idx / cols
let u0 = float(cx) / float(cols)
let v0 = float(cy) / float(rows)
let s = Math.min(w, h)
ov_sub(r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(rows), r3u_r(c), r3u_g(c), r3u_b(c), a)
}