ludic/packages/ludic.ui/attr.ludic
Orkuncakilkaya 1dad96102a 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>
2026-09-24 17:26:09 +03:00

79 lines
3.9 KiB
Text

# attr.ludic - what a property sets, by its CSS name or the short one (width or w, background or
# bg, font-size or size, ...). An attribute on an element is the same property, applied last.
const AT_ALIAS: string = " width=w height=h min-width=min-w max-width=max-w min-height=min-h max-height=max-h padding=pad background=bg background-color=bg font-size=size opacity=alpha flex-grow=grow flex-direction=dir flex-wrap=wrap justify-content=justify align-items=align align-self=self row-gap=gap column-gap=gap text-align=talign "
const AT_UNDRAWN: string = " font-weight font-family font-style box-shadow text-shadow cursor transition animation line-height letter-spacing text-decoration "
# a stylesheet's property: one that means nothing is said
function el_attr(n: UiNode, key: string, v: Val) -> void {
if el_apply(n, key, v) { return }
if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(`{key}: this renderer does not draw it`) } else { ui_err(`{key}: there is no such property`) }
}
# an element's attribute: a property when it names one, otherwise only there for [selectors]
function el_apply(n: UiNode, key: string, v0: Val) -> bool {
let k = at_alias(key)
el_cur = n
el_key = k
if len(k) > 2 and k[0..2] == "--" {
cs_var_set(n, k, vl_text(v0))
return true
}
let v = cs_vars_in(n, v0)
return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v) or el_place_prop(n, k, v) or el_edge(n, k, v)
}
function at_alias(key: string) -> string {
let pat = ` {key}=`
let n = len(pat)
for i in 0 .. len(AT_ALIAS) - n + 1 {
if AT_ALIAS[i..i + n] == pat {
var j = i + n
while AT_ALIAS[j] != 32 { j += 1 }
return AT_ALIAS[i + n..j]
}
}
return key
}
function el_size(n: UiNode, key: string, v: Val) -> bool {
if key == "w" {
n.w_pct = el_pct(v)
n.w = el_len(v)
} else if key == "h" {
n.h_pct = el_pct(v)
n.h = el_len(v)
} else if key == "min-w" { n.min_w = el_len(v) } else if key == "max-w" { n.max_w = el_len(v) } else if key == "min-h" { n.min_h = el_len(v) } else if key == "max-h" { n.max_h = el_len(v) } else if key == "gap" { n.gap = el_len(v) } else { return false }
return true
}
function el_flex(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "dir" {
n.dir = UI_COL
if s == "row" { n.dir = UI_ROW }
} else if key == "grow" or key == "flex" { n.grow = el_num(s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false }
return true
}
function el_look(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "size" { n.size = el_len(v) } else if key == "color" {
n.fg = ui_color(s)
n.fg_a = cl_alpha
} else if key == "bg" {
if len(s) > 4 and s[0..4] == "url(" { n.bg_img = el_url(s) } else {
n.bg = ui_color(s)
n.bg_a = cl_alpha
}
} else if key == "alpha" { n.alpha = el_num(s) } else if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" } else if key == "border-color" {
n.border_c = ui_color(s)
n.border_a = cl_alpha
} else if key == "border-width" { n.border = el_len(v) } else if key == "border" { el_border(n, s) } else { return false }
return true
}
# border: 1 #ffcc00, or 1px solid accent
function el_border(n: UiNode, s: string) -> void {
let parts = lss_split(s, 32)
for i in 0 .. len(parts) {
let p = parts[i]
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { n.border = el_unit(p) } else if p == "none" { n.border = 0.0 } else if len(p) > 0 and p != "solid" {
n.border_c = ui_color(p)
n.border_a = cl_alpha
}
}
if n.border_c == UI_NONE { n.border_c = n.rfg }
}