: a select steps past a disabled option (arrows, a
+# press, the pad), a value naming one is shown and set as the nearest enabled option, and the arrow
+# whose next option is disabled is drawn disabled - the options that way are off (a host's cap)
+function ct_opt_on(opts: []UiNode, i: int) -> bool { return i >= 0 and i < len(opts) and opts[i].enabled }
+
+# from `at` d steps at a time, wrapping, to the first enabled option; `at` itself when none is
+function ct_opt_step(opts: []UiNode, at: int, d: int) -> int {
+ let n = len(opts)
+ var i = at
+ for k in 0 .. n {
+ i = (i + d + n) % n
+ if opts[i].enabled { return i }
+ }
+ return at
+}
+
+# the enabled option nearest `at` (the lower of two as near); -1 when none is
+function ct_opt_nearest(opts: []UiNode, at: int) -> int {
+ if ct_opt_on(opts, at) { return at }
+ for r in 1 .. len(opts) {
+ if ct_opt_on(opts, at - r) { return at - r }
+ if ct_opt_on(opts, at + r) { return at + r }
+ }
+ return -1
+}
+
+# a select's value on a disabled option: the nearest enabled one is what it shows, and is set
+# (set once the build is done: an event fired while the tree is built is cleared with the frame's)
+function ct_opt_clamp(ui_st: mut UiState, n: UiNode, opts: []UiNode, at: int) -> int {
+ if at < 0 or ct_opt_on(opts, at) { return at }
+ let to = ct_opt_nearest(opts, at)
+ if to >= 0 {
+ ui_kept(ui_st.ct_clamp_n, n)
+ ui_kept(ui_st.ct_clamp_i, to)
+ }
+ return to
+}
+# the clamps a build found, set now that the frame's events are cleared; stale ones are dropped
+function ct_opt_flush(ui_st: mut UiState) -> void {
+ for i in 0 .. len(ui_st.ct_clamp_n) {
+ let n = ui_st.ct_clamp_n[i]
+ if n.opts != null and ui_st.ct_clamp_i[i] < len(n.opts) { ct_opt_pick(ui_st, n, n.opts, ui_st.ct_clamp_i[i]) }
+ }
+ ct_opt_forget(ui_st)
+}
+function ct_opt_forget(ui_st: mut UiState) -> void {
+ List.clear(ui_st.ct_clamp_n)
+ List.clear(ui_st.ct_clamp_i)
+}
+function ct_opt_pick(ui_st: mut UiState, n: UiNode, opts: []UiNode, at: int) -> void {
+ let ov = ui_attr(opts[at], "value")
+ if Value.kind(ov) == 0 { ct_change(ui_st, n, Value.int(at)) } else { ct_change(ui_st, n, ov) }
+}
+
+# a select: its label, < and >, and the option its value names between them (
s kept aside)
+function ct_build_select(ui_st: mut UiState, n: UiNode) -> void {
+ @alloc_ok("once per pooled node: its list is kept and cleared")
+ if n.opts == null { n.opts = new []UiNode } # its options moved to a list the node keeps, not a new one
+ let opts = n.opts
+ List.clear(opts)
+ for i in 0 .. len(n.children) { ui_kept(opts, n.children[i]) }
+ List.clear(n.children)
+ ct_label(ui_st, n)
+ var shown = ""
+ let at = ct_opt_clamp(ui_st, n, opts, ct_option_at(n, opts))
+ if at >= 0 { shown = opts[at].text }
+ let prev = ct_part(ui_st, n, "ui-prev", "<")
+ let next = ct_part(ui_st, n, "ui-next", ">")
+ if at >= 0 and len(opts) > 1 {
+ prev.enabled = ct_opt_on(opts, (at - 1 + len(opts)) % len(opts))
+ next.enabled = ct_opt_on(opts, (at + 1) % len(opts))
+ }
+ ui_kept(n.children, prev)
+ ui_kept(n.children, ct_part(ui_st, n, "ui-value", shown))
+ ui_kept(n.children, next)
+}
diff --git a/packages/ludic.ui/frame.ludic b/packages/ludic.ui/frame.ludic
index cb6f2420..d4962d74 100644
--- a/packages/ludic.ui/frame.ludic
+++ b/packages/ludic.ui/frame.ludic
@@ -5,6 +5,7 @@
export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
in_read(ui_st)
+ ct_opt_forget(ui_st) # a clamp found by a build this frame did not show
dv_before(ui_st)
ui_st.an_frame += 1
ui_viewport(ui_st, w, h)
@@ -12,6 +13,7 @@ export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: flo
if root == null { return }
ui_place(ui_st, root, x, y, w, h)
nt_fired_clear(ui_st)
+ ct_opt_flush(ui_st) # a select's value on a disabled option, set now
sc_undo(root)
sc_update(ui_st, root)
fr_pointer(ui_st, root)
diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic
index 349a9322..d10bc1d9 100644
--- a/packages/ludic.ui/index.ludic
+++ b/packages/ludic.ui/index.ludic
@@ -29,6 +29,7 @@ import "fired.ludic"
import "native_room.ludic"
import "controls.ludic"
import "controls_build.ludic"
+import "controls_opts.ludic"
import "controls_parts.ludic"
import "controls_key.ludic"
import "controls_key_mouse.ludic"
diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic
index eb5df445..615471ec 100644
--- a/tools/ludic-cli/test.ludic
+++ b/tools/ludic-cli/test.ludic
@@ -1249,6 +1249,7 @@ function cmd_dev_test() -> int {
controller_case("library/ui_select_arrows", "", "prev 0 next next 2", "ui_select_arrows.ludic (L11: a select's < steps back and its > forward)")
controller_case("library/ui_pointer_through", "", "pressed 1, hovered #4a5058; > hovered 1", "ui_pointer_through.ludic (L11: the pointer looks through a root with no size to a positioned panel's button; a select's > answers :hover)")
controller_case("library/ui_first_show", "", "bad 0 shown 3", "ui_first_show.ludic (L11: a component first shown once the fence is judging makes its props and model declared, once per instance; shown again, it is reused)")
+ controller_case("library/ui_select_disabled", "", "clamped 2 next-on 0 next 0 prev 2", "ui_select_disabled.ludic (L11:
: a value on one is set to the nearest enabled, the arrow toward them drawn disabled, the arrows step past them)")
controller_case("library/ui_submit", "", "[hi] chat '' | [hi][yo][Ada] chat '' name 'Ada'", "ui_submit.ludic (L11: on-submit on a text field by Enter or A, clear-on-submit, the focus kept)")
controller_case("library/ui_zoom", "", "plain 100 pad 10 text 16 h 20 | half 50 pad 5 text 8 h 10 | half of twice 100 | fit 50 em 16", "ui_zoom.ludic (L11: zoom scales a subtree's lengths, text and layout, multiplies, and takes min() and a length over a length)")
controller_case("library/ui_hold", "", "31 frames 500 ms t 500 | 121 frames 500 ms t 500 | pad 16 frames 250 ms t 250 | dududu", "ui_hold.ludic (L11: on-hold every frame a button is held, by the pointer or A, with dt and t on the clock at any rate)")