ludic.ui: the pointer looks through a box with no size; a control's parts answer :hover

From the user's play, reproduced on Maroon Lake's customisation screen with the pointer scripted over
each control (the lab's R3D_CLICKS): nothing on it was ever pressed or hovered.

- A screen's root <div> round a positioned panel (a component's root holding the kit Screen's
  modal) lays out to 0x0, and the hit test (fr_pick) and :hover (fr_under) only walked into a child
  whose box held the point - so nothing under it was reachable. A box with no size holds no point and
  clips nothing: both look through it now (fr_empty), without hovering it.
- A control's parts (a select's < and >, a range's thumb) are made by ct_part, which never set
  `hovered`: `select .ui-prev:hover` could not match anywhere. It is set as an element's is.

Golden ui_pointer_through: the panel's button pressed and hovered through the empty root, and a
select's > hovered (before this: pressed 0, hovered 0). The 36 ui examples pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 13:26:33 +03:00
parent 06ecc5cd8e
commit 9b3d3e3298
5 changed files with 67 additions and 2 deletions

View file

@ -0,0 +1,55 @@
# ui_pointer_through.ludic - a screen whose root <div> holds only a positioned panel (a component's
# root round a modal) has no size of its own; the pointer looks through it, so the panel's button is
# pressed and hovered - once nothing under it was. A select's < and > answer :hover as elements do.
import "ludic.ui"
program UiPointerThrough {
numbers float
state ThroughState {
got: int = 0
v: int = 1
}
view V (through_st: mut ThroughState) {
v = through_st.v
on hit() { through_st.got += 1 }
on set(x: int) { through_st.v = x }
}
const PAGE: string = "<ui><screen name='s'><div><div style='position: fixed; left: 40px; top: 40px; width: 300px; height: 200px'><button id='b' style='width: 120px; height: 40px' on-click='hit()'>Go</button><select value='{v}' on-change='set(event.value)'><option value='{0}'>A</option><option value='{1}'>B</option></select></div></div></screen></ui>"
function frame(ui_st: mut UiState, x: float, y: float, down: bool) -> UiNode {
let i = new UiInput
i.x = x
i.y = y
i.down = down
ui_input(ui_st, i)
ui_show(ui_st, "s", view_v(), 0.0, 0.0, 400.0, 400.0)
let root: UiNode = ui_nodes(ui_st, "s", view_v())
ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0)
return root
}
function find(n: UiNode, tag: string, cls: string) -> UiNode {
var has = cls == ""
for i in 0 .. len(n.classes) { if n.classes[i] == cls { has = true } }
if n.tag == tag and has { return n }
for i in 0 .. len(n.children) {
let f = find(n.children[i], tag, cls)
if f != null { return f }
}
return null
}
entry (ui_st: mut UiState, through_st: ThroughState) {
ui_backend(ui_st, new UiBackend)
ui_load_text(ui_st, PAGE, "s.xml")
var root = frame(ui_st, -1.0, -1.0, false)
let b = find(root, "button", "")
let x = b.x + b.cw / 2.0
let y = b.y + b.ch / 2.0
frame(ui_st, x, y, false)
root = frame(ui_st, x, y, false)
let hover = ui_hex(find(root, "button", "").bg)
frame(ui_st, x, y, true)
frame(ui_st, x, y, false)
let nx = find(root, "span", "ui-next")
frame(ui_st, nx.x + nx.cw / 2.0, nx.y + nx.ch / 2.0, false)
root = frame(ui_st, nx.x + nx.cw / 2.0, nx.y + nx.ch / 2.0, false)
print(`pressed {through_st.got}, hovered {hover}; > hovered {find(root, "span", "ui-next").hovered}`)
}
}