feat(ui): ludic.ui is a UI framework - input, controls, render3d backend, the CSS a game needs

A game no longer writes a host:
- The runtime reads Input itself: focus and keyboard navigation (Tab, arrows, Enter/Space,
  autofocus), the pointer (hover, :active, click on release, drag), and scroll boxes with the
  wheel, a draggable scrollbar, clipping and scroll-into-view.
- HTML's controls are built in (button, checkbox, radio, range, select, text, key capture), made
  of plain parts a stylesheet styles, each reporting with on-change and event.value.
- ludic.ui/render3d.ludic draws with render3d's overlay: textures, named atlases (icon:NAME),
  nine-slice border-image, rounded rects and rings, clipping, and a scale.
- Hooks for the program's language, sounds and clock (ui_translator, ui_sounds, ui_clock).
- ui_dev: hot reload, errors on screen, LUDIC_UI_DUMP.
- CSS:
  - colours as #rgb / #rrggbbaa / rgb() / rgba() / names;
  - border-radius and outline (following the radius), box-shadow, background-image and a tinted
    border-image;
  - group opacity, @keyframes / animation, transition;
  - :focus, :focus-visible and :focus-within.
- HTML mixed content, and boolean attributes.
- render3d gains tex_width / tex_height, and the XML reader keeps text runs in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 17:26:09 +03:00
parent 20d011f8e9
commit 1dad96102a
45 changed files with 1386 additions and 218 deletions

View file

@ -56,8 +56,12 @@ export property UiNode {
lines: []string = null # a wrapped text's lines
var_names: []string = null # its custom properties (--name), which its descendants inherit
var_vals: []string = null
fg: int = -1
bg: int = -2
fg: int = -1 # a colour is 0xRRGGBB with its alpha beside it; -1 inherits
fg_a: float = 1.0
bg: int = -2 # -2: no background
bg_a: float = 1.0
border_a: float = 1.0
rfg_a: float = 1.0
alpha: float = 1.0
enabled: bool = true
act: UiAct = null # on-press, which the pointer and the renderer's button fire
@ -76,5 +80,28 @@ export property UiNode {
rsize: float = 0.0
rfg: int = 0
hovered: bool = false
focusable: bool = false # a control, or anything with a tabindex: the keyboard reaches it
radius: float = 0.0 # border-radius
outline_w: float = 0.0 # outline: drawn outside the box, where a focus ring goes
outline_c: int = -1
outline_a: float = 1.0
outline_off: float = 0.0
scroll_y: float = 0.0 # how far a scroll box is scrolled
bar_c: int = -1 # scrollbar-color: its thumb, and its track
bar_track: int = -2
opts: []UiNode = null # a select's options
bg_img: string = "" # background-image: url(...), stretched over the box
bi_src: string = "" # border-image: a nine-slice drawn instead of the background, tinted by it
bi_slice: float = 0.0
bi_width: float = 0.0
sh_x: float = 0.0 # box-shadow: an offset copy of the box, drawn under it
sh_y: float = 0.0
sh_c: int = -2
sh_a: float = 1.0
anim: string = "" # animation: its @keyframes, how long, looping, back and forth
anim_dur: float = 0.0
anim_loop: bool = false
anim_alt: bool = false
tr_dur: float = 0.0 # transition: how long a change of opacity takes
}