ludic/packages/ludic.ui/select_match.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

102 lines
3.6 KiB
Text

# select_match.ludic - whether a selector picks a node, and how specific it is (as CSS counts)
function sel_spec(s: UiSel) -> int {
var n = 0
var at = s
while at != null {
if at.id != "" { n = n + 100 }
n = n + (len(at.classes) + len(at.akeys) + len(at.pseudos)) * 10
if at.tag != "" { n = n + 1 }
for i in 0 .. len(at.nots) { n = n + sel_spec(at.nots[i]) }
at = at.prev
}
return n
}
function sel_match(s: UiSel, n: UiNode) -> bool {
if not sel_one(s, n) { return false }
if s.prev == null { return true }
if s.comb == 1 { return n.parent != null and sel_match(s.prev, n.parent) }
if s.comb == 2 { return n.parent != null and n.index > 0 and sel_match(s.prev, n.parent.children[n.index - 1]) }
if s.comb == 3 {
if n.parent == null { return false }
for i in 0 .. n.index {
if sel_match(s.prev, n.parent.children[i]) { return true }
}
return false
}
var a = n.parent
while a != null {
if sel_match(s.prev, a) { return true }
a = a.parent
}
return false
}
function sel_one(s: UiSel, n: UiNode) -> bool {
if s.tag != "" and s.tag != n.tag { return false }
if s.id != "" and s.id != n.id { return false }
for i in 0 .. len(s.classes) {
if not sel_has(n.classes, s.classes[i]) { return false }
}
for i in 0 .. len(s.akeys) {
let v = sel_attr_of(n, s.akeys[i])
if v == null or (s.aops[i] == 1 and v != s.avals[i]) { return false }
}
for i in 0 .. len(s.pseudos) {
if not sel_pseudo_ok(s.pseudos[i], s.pargs[i], n) { return false }
}
for i in 0 .. len(s.nots) {
if sel_one(s.nots[i], n) { return false }
}
return true
}
function sel_has(xs: []string, x: string) -> bool {
if xs == null { return false }
for i in 0 .. len(xs) {
if xs[i] == x { return true }
}
return false
}
function sel_attr_of(n: UiNode, key: string) -> string {
if n.akeys == null { return null }
for i in 0 .. len(n.akeys) {
if n.akeys[i] == key { return n.avals[i] }
}
return null
}
function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
if p == "hover" { return n.hovered }
if p == "focus" { return fc_key != "" and n.key == fc_key }
if p == "focus-visible" { return fc_visible and fc_key != "" and n.key == fc_key }
if p == "focus-within" { return fc_key != "" and lc_mine(fc_key, n.key) }
if p == "disabled" { return not n.enabled }
if p == "enabled" { return n.enabled }
if p == "first-child" { return n.index == 0 }
if p == "last-child" { return n.parent != null and n.index == len(n.parent.children) - 1 }
if p == "only-child" { return n.parent != null and len(n.parent.children) == 1 }
if p == "empty" { return len(n.children) == 0 and n.text == "" }
if p == "root" { return n.parent == null }
if p == "checked" { return ui_attr_on(n, "checked") }
if p == "active" { return n.hovered and fr_down_now }
if p == "nth-child" { return sel_nth(arg, n.index + 1) }
ui_err(`:{p}: there is no such pseudo-class here`)
return false
}
# :nth-child(odd), (even), (3), (2n+1), (n+2), (-n+3): is the k-th child one of them
function sel_nth(arg: string, k: int) -> bool {
if arg == "odd" { return k % 2 == 1 }
if arg == "even" { return k % 2 == 0 }
var npos = -1
for i in 0 .. len(arg) {
if arg[i] == 110 { npos = i }
}
if npos < 0 { return string(k) == arg }
var a = 1
let head: string = arg[0..npos]
if head == "-" { a = -1 } else if len(head) > 0 { a = int(el_num(head)) }
var b = 0
var tail = tpl_words(arg[npos + 1..len(arg)])
if len(tail) > 0 and tail[0] == 43 { tail = tpl_words(tail[1..len(tail)]) }
if len(tail) > 0 { b = int(el_num(tail)) }
if a == 0 { return k == b }
let d = k - b
return d % a == 0 and d / a >= 0
}