From 0aad2294ecb26b585bfe2bcdf0c79ccb931fc67a Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Thu, 24 Sep 2026 21:48:02 +0300 Subject: [PATCH] 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 --- LANGUAGE.md | 16 +++- changes/defaults-views-templates.md | 6 ++ examples/library/ui_pointer.ludic | 60 +++++++++++++ examples/library/ui_pointer_parts/Chart.lss | 7 ++ examples/library/ui_pointer_parts/Chart.ludic | 12 +++ examples/library/ui_pointer_parts/Chart.xml | 9 ++ packages/ludic.ui/attr_more.ludic | 4 +- packages/ludic.ui/focus.ludic | 13 +++ packages/ludic.ui/index.ludic | 3 + packages/ludic.ui/input.ludic | 8 ++ packages/ludic.ui/input_pad.ludic | 8 ++ packages/ludic.ui/native.ludic | 15 +++- packages/ludic.ui/node_more.ludic | 1 + packages/ludic.ui/pointer.ludic | 43 +++++----- packages/ludic.ui/pointer_events.ludic | 85 +++++++++++++++++++ packages/ludic.ui/pointer_pick.ludic | 59 +++++++++++++ packages/ludic.ui/scroll.ludic | 2 +- packages/ludic.ui/select_match.ludic | 22 +---- packages/ludic.ui/select_nth.ludic | 21 +++++ tools/ludic-cli/test.ludic | 1 + 20 files changed, 349 insertions(+), 46 deletions(-) create mode 100644 examples/library/ui_pointer.ludic create mode 100644 examples/library/ui_pointer_parts/Chart.lss create mode 100644 examples/library/ui_pointer_parts/Chart.ludic create mode 100644 examples/library/ui_pointer_parts/Chart.xml create mode 100644 packages/ludic.ui/pointer_events.ludic create mode 100644 packages/ludic.ui/pointer_pick.ludic create mode 100644 packages/ludic.ui/select_nth.ludic diff --git a/LANGUAGE.md b/LANGUAGE.md index d802a789..69f82951 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -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 ``) 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: - `` and `` fill (`.ui-fill`) as far as their value. - **Natives** are for what markup cannot say. `ui_native("minimap", measure, draw)` makes `` 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 diff --git a/changes/defaults-views-templates.md b/changes/defaults-views-templates.md index 65043f6e..36e317f8 100644 --- a/changes/defaults-views-templates.md +++ b/changes/defaults-views-templates.md @@ -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). diff --git a/examples/library/ui_pointer.ludic b/examples/library/ui_pointer.ludic new file mode 100644 index 00000000..1b46386d --- /dev/null +++ b/examples/library/ui_pointer.ludic @@ -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}`) + } +} diff --git a/examples/library/ui_pointer_parts/Chart.lss b/examples/library/ui_pointer_parts/Chart.lss new file mode 100644 index 00000000..12ead9db --- /dev/null +++ b/examples/library/ui_pointer_parts/Chart.lss @@ -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 } diff --git a/examples/library/ui_pointer_parts/Chart.ludic b/examples/library/ui_pointer_parts/Chart.ludic new file mode 100644 index 00000000..e2717401 --- /dev/null +++ b/examples/library/ui_pointer_parts/Chart.ludic @@ -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 +} diff --git a/examples/library/ui_pointer_parts/Chart.xml b/examples/library/ui_pointer_parts/Chart.xml new file mode 100644 index 00000000..ba64f3d7 --- /dev/null +++ b/examples/library/ui_pointer_parts/Chart.xml @@ -0,0 +1,9 @@ +
+
+ + +
+
+ +

zoomed {zoomed} pad {px} {dragx} {ups} {wh} hold {holding} {held} {clicked}

+
diff --git a/packages/ludic.ui/attr_more.ludic b/packages/ludic.ui/attr_more.ludic index ada6055f..d408621d 100644 --- a/packages/ludic.ui/attr_more.ludic +++ b/packages/ludic.ui/attr_more.ludic @@ -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) diff --git a/packages/ludic.ui/focus.ludic b/packages/ludic.ui/focus.ludic index 623f3367..31d06728 100644 --- a/packages/ludic.ui/focus.ludic +++ b/packages/ludic.ui/focus.ludic @@ -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 diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index 9b2f93dc..ff86973a 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -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" diff --git a/packages/ludic.ui/input.ludic b/packages/ludic.ui/input.ludic index f0c80289..5a10ed83 100644 --- a/packages/ludic.ui/input.ludic +++ b/packages/ludic.ui/input.ludic @@ -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 diff --git a/packages/ludic.ui/input_pad.ludic b/packages/ludic.ui/input_pad.ludic index cbfa896c..faeca0eb 100644 --- a/packages/ludic.ui/input_pad.ludic +++ b/packages/ludic.ui/input_pad.ludic @@ -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 { diff --git a/packages/ludic.ui/native.ludic b/packages/ludic.ui/native.ludic index d18c03d1..df99e122 100644 --- a/packages/ludic.ui/native.ludic +++ b/packages/ludic.ui/native.ludic @@ -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) diff --git a/packages/ludic.ui/node_more.ludic b/packages/ludic.ui/node_more.ludic index 4228cee1..c38b390b 100644 --- a/packages/ludic.ui/node_more.ludic +++ b/packages/ludic.ui/node_more.ludic @@ -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 } diff --git a/packages/ludic.ui/pointer.ludic b/packages/ludic.ui/pointer.ludic index 509ba011..14a544e5 100644 --- a/packages/ludic.ui/pointer.ludic +++ b/packages/ludic.ui/pointer.ludic @@ -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) diff --git a/packages/ludic.ui/pointer_events.ludic b/packages/ludic.ui/pointer_events.ludic new file mode 100644 index 00000000..7bffe010 --- /dev/null +++ b/packages/ludic.ui/pointer_events.ludic @@ -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- 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 +} diff --git a/packages/ludic.ui/pointer_pick.ludic b/packages/ludic.ui/pointer_pick.ludic new file mode 100644 index 00000000..54d48014 --- /dev/null +++ b/packages/ludic.ui/pointer_pick.ludic @@ -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 +} diff --git a/packages/ludic.ui/scroll.ludic b/packages/ludic.ui/scroll.ludic index 22829934..22e51b4a 100644 --- a/packages/ludic.ui/scroll.ludic +++ b/packages/ludic.ui/scroll.ludic @@ -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) } diff --git a/packages/ludic.ui/select_match.ludic b/packages/ludic.ui/select_match.ludic index 35cf8267..e117db61 100644 --- a/packages/ludic.ui/select_match.ludic +++ b/packages/ludic.ui/select_match.ludic @@ -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 -} diff --git a/packages/ludic.ui/select_nth.ludic b/packages/ludic.ui/select_nth.ludic new file mode 100644 index 00000000..4c69cce6 --- /dev/null +++ b/packages/ludic.ui/select_nth.ludic @@ -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 +} diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 339279b1..dab0de09 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -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)")