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:
parent
d2b0413c30
commit
0aad2294ec
20 changed files with 349 additions and 46 deletions
16
LANGUAGE.md
16
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 `<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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
60
examples/library/ui_pointer.ludic
Normal file
60
examples/library/ui_pointer.ludic
Normal 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}`)
|
||||
}
|
||||
}
|
||||
7
examples/library/ui_pointer_parts/Chart.lss
Normal file
7
examples/library/ui_pointer_parts/Chart.lss
Normal 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 }
|
||||
12
examples/library/ui_pointer_parts/Chart.ludic
Normal file
12
examples/library/ui_pointer_parts/Chart.ludic
Normal 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
|
||||
}
|
||||
9
examples/library/ui_pointer_parts/Chart.xml
Normal file
9
examples/library/ui_pointer_parts/Chart.xml
Normal 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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
85
packages/ludic.ui/pointer_events.ludic
Normal file
85
packages/ludic.ui/pointer_events.ludic
Normal 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
|
||||
}
|
||||
59
packages/ludic.ui/pointer_pick.ludic
Normal file
59
packages/ludic.ui/pointer_pick.ludic
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
21
packages/ludic.ui/select_nth.ludic
Normal file
21
packages/ludic.ui/select_nth.ludic
Normal 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
|
||||
}
|
||||
|
|
@ -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)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue