From the fence's kept frames in 22 minutes of play: - A component field that is a record or a list of records was `value_put(o, k, view_val_T(x))`, a whole new tree every frame (HudPrompt's notifications). view_fill.ludic generates view_set_T / view_set_list_T / view_fill_T that fill the object and list under the key in place. - value_list_fit dropped the items it cut off and value_item made new ones as the list grew back, a Value per item per regrowth (value_item / value_set_strs); the cut-off items are now the list's spares (Val.spare), and an item of another kind is turned rather than replaced. - ludic.ui: a scroll box's "scroll" and a slider's "change" fired a fresh Value.float a frame (sc_walk, scroll.ludic:36); ui_fire_float takes one from a ring kept with the state (fired.ludic). ui_object_fit_into is exported, for a draw that keeps its list. - Json.read_file(path): read, parsed, and the file's text given back - Json.parse(Fs.read_text()) kept the whole file on every read (Maroon Lake's settings peeks). Golden value_list_regrow: a list alternating 6 and 2 items every frame keeps nothing (the toolchain before this fails it: +128 B new Val from value_item). Game compiles; ludic.i18n/settings/hints/ base tests pass (ludic.ui has none); arena and fence goldens unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.4 KiB
Text
79 lines
3.4 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.
|
|
function sc_get(ui_st: UiState, key: string) -> float {
|
|
for i in 0 .. len(ui_st.sc_keys) {
|
|
if ui_st.sc_keys[i] == key { return ui_st.sc_offs[i] }
|
|
}
|
|
return 0.0
|
|
}
|
|
function sc_put(ui_st: mut UiState, key: string, v: float) -> void {
|
|
for i in 0 .. len(ui_st.sc_keys) {
|
|
if ui_st.sc_keys[i] == key {
|
|
ui_st.sc_offs[i] = v
|
|
return
|
|
}
|
|
}
|
|
ui_kept(ui_st.sc_keys, key)
|
|
ui_kept(ui_st.sc_offs, v)
|
|
}
|
|
# every scroll box, outermost first: its offset (from input, when `live`), then its content moved
|
|
function sc_update(ui_st: mut UiState, n: UiNode) -> void {
|
|
ui_st.sc_scope = pp_top(n)
|
|
if not ui_st.in_now.down { ui_st.sc_drag = "" }
|
|
sc_walk(ui_st, n, true)
|
|
}
|
|
function sc_apply(ui_st: mut UiState, n: UiNode) -> void { sc_walk(ui_st, n, false) }
|
|
function sc_walk(ui_st: mut UiState, 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(ui_st, n, sc_get(ui_st, n.key), live)
|
|
var off = held
|
|
if live { off = sc_input(ui_st, n, off, room, most) }
|
|
off = Math.clamp(off, 0.0, most)
|
|
if live and off != Math.clamp(held, 0.0, most) { ui_fire_float(ui_st, n, "scroll", off / ui_px(ui_st, 1.0)) }
|
|
sc_put(ui_st, 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(ui_st, 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(ui_st: mut UiState, n: UiNode, off0: float, room: float, most: float) -> float {
|
|
var off = off0
|
|
if ui_st.sc_scope != null and not sc_within(n, ui_st.sc_scope) { return sc_into_view(ui_st, n, off, room) }
|
|
let i = ui_st.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(ui_st, 40.0) }
|
|
off = sb_input(ui_st, n, off, room, most)
|
|
return sc_into_view(ui_st, 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(ui_st: UiState, n: UiNode, off: float, room: float) -> float {
|
|
if not ui_st.fc_visible or ui_st.fc_key == "" or not lc_mine(ui_st.fc_key, n.key) { return off }
|
|
let f = fr_node(n, ui_st.fc_key)
|
|
if f == null { return off }
|
|
let top = f.y - n.y - n.pt
|
|
if top < off { return top - ui_px(ui_st, 8.0) }
|
|
if top + f.ch > off + room { return top + f.ch - room + ui_px(ui_st, 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]) }
|
|
}
|