feat(ui): pointer events for natives and elements, with capture; on-down and on-up; hits in painting order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 21:48:02 +03:00
parent d2b0413c30
commit 0aad2294ec
20 changed files with 349 additions and 46 deletions

View file

@ -441,7 +441,16 @@ applies to both:
- Tab and Shift+Tab, or the arrows, move focus through the controls in document order. Enter
and Space activate what has it, and `autofocus` picks where a screen starts.
- The pointer hovers, presses (`:active`) and clicks on release; a drag keeps the pointer until
it is let go.
it is let go. What it is on is the topmost element in painting order (positioned ones by
`z-index`), so a button over something else takes its own press; `pointer-events: none` lets
the pointer through an element.
- `on-pointerdown`, `on-pointermove`, `on-pointerup`, `on-drag` and `on-wheel` on any element
read `event.x` / `event.y` (inside the element), `event.dx` / `event.dy`, `event.button` (0 left,
1 right, 2 middle) and `event.wheel`. A press captures the pointer: its moves, drags and release
go to that element until it is let go, wherever the pointer is. A scroll box leaves the wheel to
an element inside it that takes `on-wheel`.
- `on-down` and `on-up` say when a button goes down and comes up again, by the pointer (released
anywhere) or by Enter or the pad's A held on it; it is `:active` in between.
- A scroll box (`overflow: auto` or `<scroll>`) takes the wheel, has a scrollbar that can be
dragged (`scrollbar-color`), clips what it holds and pulls the focused control into view.
- The scrollbar is `.ui-scrollbar` with a `.ui-thumb`, styled like a control's parts. A press on
@ -493,7 +502,10 @@ applies to both:
- `<progress value max>` and `<meter value min max>` fill (`.ui-fill`) as far as their value.
- **Natives** are for what markup cannot say. `ui_native("minimap", measure, draw)` makes
`<minimap>` an element the program draws itself, laid out and styled like any other. It reports
with `ui_fire(n, "change", value)` and reads its attributes with `ui_attr` / `ui_attr_on`. Its
with `ui_fire(n, "change", value)` and reads its attributes with `ui_attr` / `ui_attr_on`.
`ui_native_input("minimap", fn(n: UiNode, e: UiPointer) -> bool)` gives it the pointer events
above as a `UiPointer` (`kind`, `x`, `y`, `dx`, `dy`, `button`, `wheel`); answering false to a
`pointerdown` leaves the pointer uncaptured. Its
draw reads `ui_opacity()`, the opacity it is drawn at (every group `opacity` around it, its own
included).
- **Renderers.** `ui_backend(b)` takes any renderer with `rect`, `text` and `measure`, and uses

View file

@ -81,6 +81,12 @@ What a game's screens found missing, now in `ludic.ui`:
- the first gamepad moves the focus (d-pad, left stick), steps ranges and selects, and presses (A)
and goes back (B); a held direction, on the pad or the arrow keys, repeats after 0.42 s and then
every 0.11 s on the ui clock (`UiInput.held_*`, `pad_a`, `pad_b` for a host);
- pointer events: `on-pointerdown` / `pointermove` / `pointerup` / `drag` / `wheel` with `event.x`,
`y`, `dx`, `dy`, `button` and `wheel`, and `ui_native_input(tag, fn)` for a native; a press captures
the pointer until release; the pointer hits the topmost element in painting order, and
`pointer-events: none` lets it through;
- `on-down` and `on-up` on a button (the pointer, Enter or A), with `:active` true while it is held
there rather than whenever the pointer is down over it;
- a transition lands exactly on its end value (it had stopped a rounding error short of it, at every
frame rate).

View file

@ -0,0 +1,60 @@
# ui_pointer.ludic — pointer events: a native map takes presses, drags (still its own when the
# pointer leaves it, until let go), the wheel and the release, but not a press on the button over it;
# a pad takes the same in markup; a hold button says on-down and on-up, and is :active while held.
import "ludic.ui"
import "ui_pointer_parts/Chart.ludic"
program UiPointer {
numbers float
var log: string = ""
var dragged: float = 0.0
function map_measure(n: UiNode, avail: float) -> void { }
function map_draw(n: UiNode) -> void { }
function map_input(n: UiNode, e: UiPointer) -> bool {
if e.kind == "drag" { dragged = dragged + e.dx } else if e.kind != "pointermove" { log = log + `{e.kind} {int(e.x)},{int(e.y)} {e.button} {int(e.wheel)}; ` }
return true
}
function frame(x: float, y: float, down: bool, wheel: float, enter: bool) -> UiNode {
let i = new UiInput
i.x = x
i.y = y
i.down = down
i.wheel = wheel
i.held_enter = enter
ui_input(i)
ui_show("Chart", null, 0.0, 0.0, 400.0, 400.0)
let root: UiNode = ui_nodes("Chart", null)
ui_place(root, 0.0, 0.0, 400.0, 400.0)
return root.children[0]
}
function said(page: UiNode) -> string { return page.children[len(page.children) - 1].text }
entry {
ui_backend(new UiBackend)
ui_native("chartmap", fn map_measure, fn map_draw)
ui_native_input("chartmap", fn map_input)
let page = frame(-1.0, -1.0, false, 0.0, false)
let box = page.children[0]
let pad = page.children[1]
let hold = page.children[2]
frame(box.x + 20.0, box.y + 20.0, true, 0.0, false)
frame(box.x + 20.0, box.y + 20.0, false, 0.0, false)
frame(box.x + 100.0, box.y + 80.0, true, 0.0, false)
frame(box.x + 130.0, box.y + 60.0, true, 0.0, false)
frame(box.x + 300.0, box.y + 300.0, true, 0.0, false)
frame(box.x + 300.0, box.y + 300.0, false, 0.0, false)
frame(box.x + 50.0, box.y + 50.0, false, 1.0, false)
frame(pad.x + 7.0, pad.y + 9.0, true, 0.0, false)
frame(pad.x + 17.0, pad.y + 9.0, true, 0.0, false)
frame(pad.x + 17.0, pad.y + 9.0, false, 0.0, false)
frame(pad.x + 17.0, pad.y + 9.0, false, 2.0, false)
frame(hold.x + 5.0, hold.y + 5.0, true, 0.0, false)
let down = frame(hold.x + 5.0, hold.y + 5.0, true, 0.0, false)
let active = down.children[2].bg
let mid = said(down)
frame(hold.x + 5.0, hold.y + 90.0, false, 0.0, false)
let off = frame(-1.0, -1.0, false, 0.0, false)
frame(-1.0, -1.0, false, 0.0, true)
let keyed = said(frame(-1.0, -1.0, false, 0.0, true))
let up = frame(-1.0, -1.0, false, 0.0, false)
print(`{log}dragged {int(dragged)} | {mid} active {active} | {said(off)} | {keyed} | {said(up)} {up.children[2].bg}`)
}
}

View file

@ -0,0 +1,7 @@
.page { width: 400px; gap: 4px }
.box { width: 200px; height: 100px; position: relative }
.map { width: 200px; height: 100px }
.zoom { position: absolute; top: 10px; left: 10px; width: 30px; height: 30px; padding: 0 }
.pad { width: 100px; height: 50px }
.hold { width: 80px; height: 30px }
.hold:active { background: #ff0000 }

View file

@ -0,0 +1,12 @@
# Chart.ludic - a native map with a zoom button over it, a pad that takes pointer events in markup,
# and a hold button that says when it goes down and comes up
component Chart {
state zoomed: int = 0
state px: float = -1.0
state dragx: float = 0.0
state ups: int = 0
state wh: float = 0.0
state holding: bool = false
state held: int = 0
state clicked: int = 0
}

View file

@ -0,0 +1,9 @@
<div class="page">
<div class="box">
<chartmap class="map"/>
<button class="zoom" on-click="set zoomed = zoomed + 1">+</button>
</div>
<div class="pad" on-pointerdown="set px = event.x" on-drag="set dragx = dragx + event.dx" on-pointerup="set ups = ups + 1" on-wheel="set wh = wh + event.wheel"></div>
<button class="hold" on-down="set holding = true" on-up="set holding = false; set held = held + 1" on-click="set clicked = clicked + 1">Hold</button>
<p>zoomed {zoomed} pad {px} {dragx} {ups} {wh} hold {holding} {held} {clicked}</p>
</div>

View file

@ -1,8 +1,8 @@
# attr_more.ludic - width and height (where 0 is 0 and calc() may hold a percentage), the flex
# shorthand, flex-shrink, order and text-shadow
# shorthand, flex-shrink, order, text-shadow and pointer-events
function el_more(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(n, s) } else { return false }
if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(n, s) } else if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else { return false }
return true
}
# width or height: a length (0 included), fill, a percentage, calc(), or auto/fit (none)

View file

@ -42,11 +42,24 @@ function fc_keys(root: UiNode) -> void {
if i.down_key or (i.right and not horizontal) { fc_move(1) }
ct_num_blur(root)
if n == null { return }
fc_hold(root, n)
if i.enter or (i.space and not ct_takes_space(n)) { ct_activate(n) }
if horizontal and i.left { ct_step(n, -1) }
if horizontal and i.right { ct_step(n, 1) }
ct_typed(n)
}
# Enter or A held on a control: its on-down, and on-up when let go (or the focus moves away)
function fc_hold(root: UiNode, n: UiNode) -> void {
if fr_key_press != "" and (in_act_up or n.key != fr_key_press) {
let was = fr_node(root, fr_key_press)
if was != null { ui_fire(was, "up", Value.null()) }
fr_key_press = ""
}
if in_act_down and n.enabled {
fr_key_press = n.key
ui_fire(n, "down", Value.null())
}
}
export function ui_focus(key: string) -> void {
fc_key = key
fc_visible = false

View file

@ -62,6 +62,7 @@ import "media.ludic"
import "select.ludic"
import "select_parts.ludic"
import "select_match.ludic"
import "select_nth.ludic"
import "cascade.ludic"
import "cascade_merge.ludic"
import "layout.ludic"
@ -72,6 +73,8 @@ import "layout_run.ludic"
import "layout_wrap.ludic"
import "frame.ludic"
import "pointer.ludic"
import "pointer_pick.ludic"
import "pointer_events.ludic"
import "screen_class.ludic"
import "draw.ludic"
import "draw_content.ludic"

View file

@ -24,9 +24,13 @@ export property UiInput {
held_right: bool = false
pad_a: bool = false # the pad's south button held: Enter
pad_b: bool = false # its east button held: Escape
held_enter: bool = false # Enter held (with pad_a: a button's on-down, then its on-up)
down_right: bool = false # the other mouse buttons held
down_middle: bool = false
}
var in_now: UiInput = new UiInput
var in_was_down: bool = false
var in_last: UiInput = new UiInput
var in_given: bool = false
function in_read() -> void {
if in_given {
@ -34,11 +38,14 @@ function in_read() -> void {
return
}
in_was_down = in_now.down
in_last = in_now
let i = new UiInput
if ui_be != null and ui_be.input {
i.x = float(Input.mouse_x())
i.y = float(Input.mouse_y())
i.down = Input.mouse_down(0)
i.down_right = Input.mouse_down(1)
i.down_middle = Input.mouse_down(2)
i.wheel = float(Input.wheel())
i.tab = Input.key_pressed(Key.Tab)
i.shift = Input.key_down(Key.Shift)
@ -66,6 +73,7 @@ function in_any_key() -> int {
# this frame's input, given by a test or a host that reads its own
export function ui_input(i: UiInput) -> void {
in_was_down = in_now.down
in_last = in_now
in_repeat(i)
in_now = i
in_given = true

View file

@ -7,6 +7,9 @@ const PAD_STICK: float = 0.5
var rp_next: []float = [-1.0, -1.0, -1.0, -1.0]
var rp_a: bool = false
var rp_b: bool = false
var rp_act: bool = false
var in_act_down: bool = false # A or Enter went down this frame, or came up
var in_act_up: bool = false
# what is held this frame: the arrow keys, the d-pad (SDL's order: 11 up, 12 down, 13 left, 14
# right) and the stick past half way along its larger axis
function in_held(i: UiInput) -> void {
@ -14,6 +17,7 @@ function in_held(i: UiInput) -> void {
i.held_down = Input.key_down(Key.Down)
i.held_left = Input.key_down(Key.Left)
i.held_right = Input.key_down(Key.Right)
i.held_enter = Input.key_down(Key.Enter)
if not Input.pad_connected(0) { return }
let ax = float(Input.pad_axis(0, 0))
let ay = float(Input.pad_axis(0, 1))
@ -36,6 +40,10 @@ function in_repeat(i: UiInput) -> void {
if i.pad_b and not rp_b { i.escape = true }
rp_a = i.pad_a
rp_b = i.pad_b
let act = i.pad_a or i.held_enter
in_act_down = act and not rp_act
in_act_up = rp_act and not act
rp_act = act
}
function rp_fire(k: int, held: bool, now: float) -> bool {
if not held {

View file

@ -6,6 +6,7 @@ export property UiNative {
tag: string = ""
measure: fn(UiNode, float) -> void = null # the content's size into n.mw, n.mh; avail is the room
draw: fn(UiNode) -> void = null
input: fn(UiNode, UiPointer) -> bool = null # the pointer on it (pointer_events.ludic)
}
var nt_list: []UiNative = new []UiNative
export function ui_native(tag: string, measure: fn(UiNode, float) -> void, draw: fn(UiNode) -> void) -> void {
@ -15,12 +16,23 @@ export function ui_native(tag: string, measure: fn(UiNode, float) -> void, draw:
nv.draw = draw
for i in 0 .. len(nt_list) {
if nt_list[i].tag == tag {
nv.input = nt_list[i].input
nt_list[i] = nv
return
}
}
push(nt_list, nv)
}
# the pointer on a native: its presses, moves, drags, releases and the wheel, while it is over it or
# has it captured; answering false to a pointerdown leaves the pointer free
export function ui_native_input(tag: string, f: fn(UiNode, UiPointer) -> bool) -> void {
let nv = nt_find(tag)
if nv == null {
ui_err(`ui_native_input: there is no native <{tag}> - ui_native it first`)
return
}
nv.input = f
}
function nt_find(tag: string) -> UiNative {
for i in 0 .. len(nt_list) {
if nt_list[i].tag == tag { return nt_list[i] }
@ -52,7 +64,8 @@ function nt_run(n: UiNode, name: string, value: Val) -> void {
if n.on_names == null or not n.enabled { return }
for i in 0 .. len(n.on_names) {
if n.on_names[i] == name {
let ev = Value.object()
var ev = Value.object()
if lk_in(PE_NAMES, name) and Value.kind(value) == 6 { ev = value }
Value.put(ev, "name", Value.str(name))
Value.put(ev, "value", value)
let e = env_new(n.env)

View file

@ -18,6 +18,7 @@ export property UiNodeX {
tsh_y: float = 0.0
tsh_c: int = -2
tsh_a: float = 1.0
no_pointer: bool = false # pointer-events: none - the pointer goes through it
}
function nx(n: UiNode) -> UiNodeX {
if n.ex == null { n.ex = new UiNodeX }

View file

@ -4,6 +4,7 @@
var fr_hover: []string = new []string
var fr_press_key: string = ""
var fr_down_now: bool = false
var fr_key_press: string = "" # the control Enter or the pad's A is holding down
function fr_was_hovered(key: string) -> bool { return sel_has(fr_hover, key) }
function fr_pointer(root: UiNode) -> void {
let i = in_now
@ -18,40 +19,44 @@ function fr_pointer(root: UiNode) -> void {
if top != null and pp_pointer(top) { return }
if sc_drag != "" { return }
let hit = fr_hit(scope, i.x, i.y)
if i.down and not in_was_down {
ct_key_pressed_away(hit)
fr_press_key = ""
if hit != null {
fr_press_key = hit.key
if hit.focusable { ui_focus(hit.key) }
ct_press(hit, i.x, i.y)
}
}
pe_frame(root, scope)
if i.down and not in_was_down { fr_press(hit, i.x, i.y) }
if i.down and fr_press_key != "" { ct_drag(root, fr_press_key, i.x, i.y) }
if not i.down and in_was_down {
let was = fr_node(root, fr_press_key)
if was != null { ui_fire(was, "up", Value.null()) }
if hit != null and hit.key == fr_press_key and hit.enabled { ct_activate(hit) }
fr_press_key = ""
}
}
function fr_press(hit: UiNode, x: float, y: float) -> void {
ct_key_pressed_away(hit)
fr_press_key = ""
if hit == null { return }
fr_press_key = hit.key
if hit.focusable { ui_focus(hit.key) }
if hit.enabled { ui_fire(hit, "down", Value.null()) }
ct_press(hit, x, y)
}
# :active - the press began on it, or inside it, and is still held (by the pointer, Enter or A)
function fr_active(n: UiNode) -> bool {
if fr_down_now and fr_press_key != "" and lc_mine(fr_press_key, n.key) { return true }
if pe_key != "" and lc_mine(pe_key, n.key) { return true }
return fr_key_press != "" and lc_mine(fr_key_press, n.key)
}
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return }
if n.key != "" { push(keys, n.key) }
for i in 0 .. len(n.children) { fr_under(n.children[i], px, py, keys) }
}
# the deepest control (or pressable element) under the pointer
# the control (or pressable element) the pointer is on: out from the topmost element under it
function fr_hit(n: UiNode, px: float, py: float) -> UiNode {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return null }
var i = len(n.children) - 1
while i >= 0 {
let h = fr_hit(n.children[i], px, py)
if h != null { return h }
i -= 1
}
if n.focusable or n.act != null { return n }
return null
if not fr_in(n, px, py) { return null }
return fr_pick(n, px, py, PK_CONTROL)
}
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }
function fr_node(n: UiNode, key: string) -> UiNode {
if key == "" { return null }
if n.key == key { return n }
for i in 0 .. len(n.children) {
let f = fr_node(n.children[i], key)

View file

@ -0,0 +1,85 @@
# pointer_events.ludic - on-pointerdown, on-pointermove, on-pointerup, on-wheel and on-drag on any
# element, with event.x / event.y inside it, event.dx / event.dy, event.button and event.wheel; a
# native takes the same through ui_native_input. A press captures the pointer until it is let go.
export property UiPointer {
kind: string = "" # pointerdown, pointermove, pointerup, wheel or drag
x: float = 0.0 # inside the element
y: float = 0.0
dx: float = 0.0 # how far the pointer moved this frame
dy: float = 0.0
button: int = 0 # 0 the left button, 1 the right, 2 the middle
wheel: float = 0.0
}
const PE_NAMES: string = " pointerdown pointermove pointerup wheel drag "
var pe_key: string = "" # the element the pointer is captured by
var pe_btn: int = 0
function pe_wants(n: UiNode, wheel: bool) -> bool {
if n.kind == UI_NATIVE and n.native != null and n.native.input != null { return true }
if n.on_names == null { return false }
for i in 0 .. len(n.on_names) {
if wheel and n.on_names[i] == "wheel" { return true }
if not wheel and lk_in(PE_NAMES, n.on_names[i]) { return true }
}
return false
}
# the pointer's events this frame: to what captured it, else to what it is over
function pe_frame(root: UiNode, scope: UiNode) -> void {
let i = in_now
let p = in_last
let moved = p.x >= 0.0 and (i.x != p.x or i.y != p.y)
var over: UiNode = null
if fr_in(scope, i.x, i.y) { over = fr_pick(scope, i.x, i.y, PK_POINTER) }
if pe_key != "" {
let c = fr_node(root, pe_key)
if c != null and moved { pe_send(c, "pointermove", pe_btn) }
if c != null and moved { pe_send(c, "drag", pe_btn) }
if not (i.down or i.down_right or i.down_middle) {
if c != null { pe_send(c, "pointerup", pe_btn) }
pe_key = ""
}
} else if over != null {
let b = pe_pressed(i, p)
if b >= 0 { pe_down(over, b) } else if moved { pe_send(over, "pointermove", 0) }
}
if i.wheel != 0.0 and over != null { pe_send(over, "wheel", 0) }
}
function pe_pressed(i: UiInput, p: UiInput) -> int {
if i.down and not p.down { return 0 }
if i.down_right and not p.down_right { return 1 }
if i.down_middle and not p.down_middle { return 2 }
return -1
}
function pe_down(n: UiNode, b: int) -> void {
pe_key = n.key
pe_btn = b
if not pe_send(n, "pointerdown", b) { pe_key = "" }
}
# one event: a native's input answers at once (false: not its to take); an element's on-<kind> runs
# once the frame is drawn, with the pointer's fields on `event`
function pe_send(n: UiNode, kind: string, b: int) -> bool {
let e = new UiPointer
e.kind = kind
e.x = in_now.x - n.x
e.y = in_now.y - n.y
if in_last.x >= 0.0 {
e.dx = in_now.x - in_last.x
e.dy = in_now.y - in_last.y
}
e.button = b
e.wheel = in_now.wheel
if n.kind == UI_NATIVE and n.native != null and n.native.input != null { return n.native.input(n, e) }
let v = Value.object()
Value.put(v, "x", Value.float(e.x))
Value.put(v, "y", Value.float(e.y))
Value.put(v, "dx", Value.float(e.dx))
Value.put(v, "dy", Value.float(e.dy))
Value.put(v, "button", Value.int(b))
Value.put(v, "wheel", Value.float(e.wheel))
ui_fire(n, kind, v)
return true
}
# a scroll box leaves the wheel to what is under the pointer inside it and takes it itself
function pe_takes_wheel(box: UiNode, px: float, py: float) -> bool {
let t = fr_pick(box, px, py, PK_WHEEL)
return t != null and t != box
}

View file

@ -0,0 +1,59 @@
# pointer_pick.ludic - what the pointer is on: the topmost element under it in painting order (a
# positioned one by its z-index over the flow), and from there out to the nearest element that wants
# what is asked - a control to press, or pointer events. pointer-events: none lets it through.
const PK_CONTROL: int = 0
const PK_POINTER: int = 1
const PK_WHEEL: int = 2
function fr_in(n: UiNode, px: float, py: float) -> bool {
if n.ex != null and n.ex.no_pointer { return false }
return px >= n.x and py >= n.y and px < n.x + n.cw and py < n.y + n.ch
}
# the nearest element that wants `kind`, out from the topmost one under the point
function fr_pick(n: UiNode, px: float, py: float, kind: int) -> UiNode {
let kids = fr_paint_rev(n)
for i in 0 .. len(kids) {
if fr_in(kids[i], px, py) {
let r = fr_pick(kids[i], px, py, kind)
if r != null { return r }
return fr_wants(n, kind)
}
}
return fr_wants(n, kind)
}
function fr_wants(n: UiNode, kind: int) -> UiNode {
if kind == PK_CONTROL and (n.focusable or n.act != null) { return n }
if kind == PK_POINTER and pe_wants(n, false) { return n }
if kind == PK_WHEEL and pe_wants(n, true) { return n }
return null
}
# the children the other way from how they are painted: positioned ones by z-index, highest first,
# then the flow, last first; a popover is on the top layer, not here
function fr_paint_rev(n: UiNode) -> []UiNode {
let out = new []UiNode
if n.children == null { return out }
var lo = 1000000
var hi = -1000000
for i in 0 .. len(n.children) {
let c = n.children[i]
if c.pos != 0 and not c.popover {
lo = Math.min(lo, c.z)
hi = Math.max(hi, c.z)
}
}
var z = hi
while z >= lo {
var i = len(n.children) - 1
while i >= 0 {
let c = n.children[i]
if c.pos != 0 and not c.popover and c.z == z { push(out, c) }
i -= 1
}
z -= 1
}
var k = len(n.children) - 1
while k >= 0 {
if n.children[k].pos == 0 and not n.children[k].popover { push(out, n.children[k]) }
k -= 1
}
return out
}

View file

@ -50,7 +50,7 @@ function sc_input(n: UiNode, off0: float, room: float, most: float) -> float {
if sc_scope != null and not sc_within(n, sc_scope) { return sc_into_view(n, off, room) }
let i = in_now
let over = i.x >= n.x and i.y >= n.y and i.x < n.x + n.cw and i.y < n.y + n.ch
if over and i.wheel != 0.0 { off = off - i.wheel * ui_px(40.0) }
if over and i.wheel != 0.0 and not pe_takes_wheel(n, i.x, i.y + off0) { off = off - i.wheel * ui_px(40.0) }
off = sb_input(n, off, room, most)
return sc_into_view(n, off, room)
}

View file

@ -75,28 +75,8 @@ function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
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 == "active" { return fr_active(n) }
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
}

View file

@ -0,0 +1,21 @@
# select_nth.ludic - :nth-child's formulas
# :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
}

View file

@ -764,6 +764,7 @@ function cmd_dev_test() -> int {
controller_case("library/ui_scrollbar", "", "grab 0 moved 120 far 300 back 60 kept 60 | jump 270 | wheel 230 | pressed 1", "ui_scrollbar.ludic (L11: a scrollbar's thumb held where it was taken and dragged, a press on the track jumps there, the wheel, nothing under the bar pressed)")
controller_case("library/ui_ease", "", "60: 1480 680 slid 1 landed 1 left 0; 120: 1480 680 slid 1 landed 1 left 0; 180: 1480 680 slid 1 landed 1 left 0; 240: 1480 680 slid 1 landed 1 left 0;", "ui_ease.ludic (L11: a transition and an animation at 60, 120, 180 and 240 Hz are the same half way and land exactly on their targets)")
controller_case("library/ui_gamepad", "", "last 6 vol 0 q 0 open false | last 0 vol 0 q 0 open false | last 0 vol 2 q 0 open false | last 0 vol 2 q 2 open false | last 0 vol 2 q 2 open true | last 0 vol 2 q 2 open false", "ui_gamepad.ludic (L11: the pad and held keys - a held direction repeats on the clock at any rate, A presses, left and right step, B closes a popover)")
controller_case("library/ui_pointer", "", "pointerdown 100,80 0 0; pointerup 300,300 0 0; wheel 50,50 0 1; dragged 200 | zoomed 1 pad 7 10 1 2 hold true 0 0 active 16711680 | zoomed 1 pad 7 10 1 2 hold false 1 0 | zoomed 1 pad 7 10 1 2 hold true 1 0 | zoomed 1 pad 7 10 1 2 hold false 2 0 3817286", "ui_pointer.ludic (L11: pointer events for natives and markup, capture until release, a button over a native keeps its press; on-down, on-up and :active)")
controller_case("library/ui_inputs", "", "count 12 12 32 0.5 0 | 20 32 0.5 0 | 19 32 0.5 0 | How many to carry | Space bar listening 1 19 9 0.5 0 | 19 9 0.5 0 | 19 0 0.5 0 | 60% 0.50 s | 19 0 0.6 9 | 7!", "ui_inputs.ludic (L11: number fields, key capture with Esc and Backspace, notes, range formats, an event named set, a string prop given a number)")
controller_case("library/ui_boxes", "", "list 120 of 200 | text at 32 152 wide, 3 lines, row 60 | zero 0x0 | half 140x36 pad 4 2 | order abc at 16 | fill 98x8 of 200x12", "ui_boxes.ludic (L11: flex-shrink to the leftover room, text wrapping beside its siblings, zero sizes, calc(), order, the range track as a row)")
controller_case("library/ui_look", "", "root div.in.i.m pad 5 margin 9 color #ff0000 text Hi 2,53 #102030 | text Hi 0,50 #eeeeee | opacity 0.25 | nine 40 #ffffff nine 40 #ff0000 of 2 image photo.png #ffffff | image icon:tent #00ff00 | text first line 29,205 #eeeeee | text second line 29,223 #eeeeee by id at 84,50 | flipped at 246,268 100x32", "ui_look.ludic (L11: nested components styled by specificity, text-shadow, ui_opacity, border-image tints, pictures, tooltip lines, anchored popovers)")