ludic/packages/ludic.ui/scroll.ludic
2026-09-24 22:00:21 +03:00

83 lines
3.2 KiB
Text

# scroll.ludic - a box with overflow (or <scroll>) scrolls itself: the wheel over it, its scrollbar
# held and dragged, and the focused control pulled into view. Its content is shifted by how far it is
# scrolled and clipped to it, and the offset is kept by its key from frame to frame.
var sc_keys: []string = new []string
var sc_offs: []float = new []float
var sc_drag: string = ""
var sc_scope: UiNode = null # the popover that has the pointer this frame, if one is up
function sc_get(key: string) -> float {
for i in 0 .. len(sc_keys) {
if sc_keys[i] == key { return sc_offs[i] }
}
return 0.0
}
function sc_put(key: string, v: float) -> void {
for i in 0 .. len(sc_keys) {
if sc_keys[i] == key {
sc_offs[i] = v
return
}
}
push(sc_keys, key)
push(sc_offs, v)
}
# every scroll box, outermost first: its offset (from input, when `live`), then its content moved
function sc_update(n: UiNode) -> void {
sc_scope = pp_top(n)
if not in_now.down { sc_drag = "" }
sc_walk(n, true)
}
function sc_apply(n: UiNode) -> void { sc_walk(n, false) }
function sc_walk(n: UiNode, live: bool) -> void {
if n.kind == UI_SCROLL {
let room = n.ch - ly_vin(n)
let most = Math.max(n.content_h - ly_vin(n) - room, 0.0)
let held = sc_held(n, sc_get(n.key), live)
var off = held
if live { off = sc_input(n, off, room, most) }
off = Math.clamp(off, 0.0, most)
if live and off != Math.clamp(held, 0.0, most) { ui_fire(n, "scroll", Value.float(off / ui_px(1.0))) }
sc_put(n.key, off)
n.scroll_y = off
if off != 0.0 {
for k in 0 .. len(n.children) { ly_offset(n.children[k], 0.0, 0.0 - off) }
}
}
for k in 0 .. len(n.children) { sc_walk(n.children[k], live) }
}
# the wheel over it, its bar held and dragged (scroll_bar.ludic), the keyboard's focus pulled into
# view; while a popover is up, only a box inside it answers the pointer
function sc_input(n: UiNode, off0: float, room: float, most: float) -> float {
var off = off0
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 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)
}
function sc_within(n: UiNode, top: UiNode) -> bool {
var at = n
while at != null {
if at == top { return true }
at = at.parent
}
return false
}
# the focused control, moved by the keyboard, is brought into the box's view
function sc_into_view(n: UiNode, off: float, room: float) -> float {
if not fc_visible or fc_key == "" or not lc_mine(fc_key, n.key) { return off }
let f = fr_node(n, fc_key)
if f == null { return off }
let top = f.y - n.y - n.pt
if top < off { return top - ui_px(8.0) }
if top + f.ch > off + room { return top + f.ch - room + ui_px(8.0) }
return off
}
# ui_place moved every box by its stored offset; the frame takes that back before moving it again
function sc_undo(n: UiNode) -> void {
if n.kind == UI_SCROLL and n.scroll_y != 0.0 {
for k in 0 .. len(n.children) { ly_offset(n.children[k], 0.0, n.scroll_y) }
}
for k in 0 .. len(n.children) { sc_undo(n.children[k]) }
}