ludic/packages/ludic.ui/controls_build.ludic
Orkuncakilkaya 181d317d20 ludic.ui: <option disabled="{...}"> - a select steps past it, clamps off it and greys the arrow toward it
- The arrows, a press on the select and the pad step past a disabled option (ct_opt_step), wrapping
  as before.
- A value naming a disabled option is shown as the nearest enabled one (the lower of two as near) and
  set to it once the build is done: an event fired while the tree is built is cleared with the frame's,
  so the clamp is queued (ct_clamp_n / _i) and fired after nt_fired_clear.
- The < or > whose next option is disabled is drawn disabled (its part's :disabled), so a cycler shows
  where the options stop being on; it still steps past them.
- ct_build_select moves to controls_opts.ludic with the rest (controls_build.ludic 119 -> 104 lines).

Golden ui_select_disabled: options 3 and 4 disabled, a value of 4 clamps to 2, > is drawn disabled
there, > wraps to 0 and < from 0 steps back to 2. The 38 ui examples pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:01:43 +03:00

104 lines
4.1 KiB
Text

# controls_build.ludic - a control's parts, made as plain child boxes a stylesheet can style:
# input[type=checkbox|radio] .ui-label .ui-track > .ui-knob
# input[type=range] .ui-label .ui-track > .ui-fill, .ui-thumb .ui-value
# input[type=text|number|key] .ui-label .ui-field > .ui-value (+ .ui-caret while focused)
# select .ui-label .ui-prev .ui-value .ui-next (its <option>s hidden)
# with a note="...", the label is .ui-labels > .ui-label + .ui-note
function ct_part(ui_st: mut UiState, n: UiNode, cls: string, text: string) -> UiNode {
let p = nd_take(ui_st)
p.key = mm_join(ui_st, n.key, "/", cls)
p.hovered = fr_was_hovered(ui_st, p.key) # a part answers :hover as an element does (a select's < and >)
p.tag = "span"
p.env = n.env
p.sheet = n.sheet
ui_kept(p.classes, cls)
if text != "" {
p.kind = UI_TEXT
p.text = text
}
return p
}
# <progress value max> and <meter value min max>: a track with a fill as far as the value
function ct_build_bar(ui_st: mut UiState, n: UiNode) -> void {
n.dir = UI_ROW
let lo = ui_attr_float(n, "min", 0.0)
var hi = ui_attr_float(n, "max", 0.0)
if hi <= lo { hi = lo + 1.0 }
let fill = ct_part(ui_st, n, "ui-fill", "")
fill.w_pct = Math.clamp((ui_attr_float(n, "value", 0.0) - lo) / (hi - lo), 0.0, 1.0) * 100.0
ui_kept(n.children, fill)
}
function ct_build(ui_st: mut UiState, n: UiNode) -> void {
if n.tag == "progress" or n.tag == "meter" {
ct_build_bar(ui_st, n)
return
}
n.dir = UI_ROW
n.focusable = true
let t = ui_attr_text(n, "type")
if n.tag == "select" {
ct_build_select(ui_st, n)
return
}
ct_label(ui_st, n)
if t == "checkbox" or t == "radio" {
let track = ct_part(ui_st, n, "ui-track", "")
ui_kept(track.children, ct_part(ui_st, n, "ui-knob", ""))
ui_kept(n.children, track)
} else if t == "range" {
ct_build_range(ui_st, n)
} else {
let field = ct_part(ui_st, n, "ui-field", "")
ui_kept(field.children, ct_part(ui_st, n, "ui-value", ct_field_text(ui_st, n)))
if ui_st.fc_key == n.key and t != "key" { ui_kept(field.children, ct_part(ui_st, n, "ui-caret", "")) }
ui_kept(n.children, field)
}
}
function ct_build_range(ui_st: mut UiState, n: UiNode) -> void {
let lo = ui_attr_float(n, "min", 0.0)
var hi = ui_attr_float(n, "max", 0.0)
if hi <= lo { hi = lo + 1.0 }
let v = ui_attr_float(n, "value", 0.0)
let track = ct_part(ui_st, n, "ui-track", "")
let fill = ct_part(ui_st, n, "ui-fill", "")
fill.w_pct = Math.clamp((v - lo) / (hi - lo), 0.0, 1.0) * 100.0
ui_kept(track.children, fill)
ui_kept(track.children, ct_part(ui_st, n, "ui-thumb", ""))
ui_kept(n.children, track)
ui_kept(n.children, ct_part(ui_st, n, "ui-value", ct_value_shown(ui_st, n)))
}
# which option the select's value names: an option's value="...", else its place
function ct_option_at(n: UiNode, opts: []UiNode) -> int {
let nv = ui_attr(n, "value")
if ct_numeric(nv) { return ct_option_num(nv, opts) }
let v = ui_attr_text(n, "value")
for i in 0 .. len(opts) {
let ov = ui_attr_text(opts[i], "value")
if ov == v or (ov == "" and ct_is_int(v, i)) { return i }
}
return -1
}
function ct_field_text(ui_st: mut UiState, n: UiNode) -> string {
let t = ui_attr_text(n, "type")
if t == "key" { return ct_key_text(ui_st, n) }
if t == "number" { return ct_num_shown(ui_st, n) }
return ui_attr_text(n, "value")
}
# a number held by a select (the usual case: a setting's value) matched as a number: its text was
# made every frame for every select on screen. An option with no value of its own is its index
function ct_numeric(v: Val) -> bool { return Value.kind(v) == 1 or Value.kind(v) == 7 }
function ct_option_num(nv: Val, opts: []UiNode) -> int {
let x = Value.as_float(nv)
for i in 0 .. len(opts) {
if not ui_attr_has(opts[i], "value") {
if x == float(i) { return i }
continue
}
let ov = ui_attr(opts[i], "value")
if Value.kind(ov) == 0 and x == float(i) { return i }
if ct_numeric(ov) and Value.as_float(ov) == x { return i }
if Value.kind(ov) == 4 and el_num(Value.as_str(ov)) == x and len(Value.as_str(ov)) > 0 { return i }
}
return -1
}