lingmodeler
↓ 0 downloadsLingModeler: box modeling with quads and tris, rigging + weight painting, 2D vector illustration and animation, procedural textures and game assets; .lmesh / .ling native formats, OBJ, glTF and SVG import / export
Keywords 3d, modeling, vector, svg, gltf, obj, rigging, animation, textures, ling
lingfu install lingmodelerCode / Files (67)
ling/core.ling
# ━━━ core.ling — input, hit-testing, drawing helpers, widget toolkit (LingStationX lineage) ━━━
# All app state lives in the native store (st_get / st_set) so plain
# top-level functions can share it. Colours are packed numbers r*65536+g*256+b.
# ── frame begin / end ────────────────────────────────────────────────────────
fn u_begin() {
bind mx = mouse_x()
bind my = mouse_y()
bind dn = 0.0
if mouse_down() { bind dn = 1.0 }
bind rdn = 0.0
if mouse_down_right() { bind rdn = 1.0 }
bind mdn = 0.0
if mouse_down_middle() { bind mdn = 1.0 }
bind scr = mouse_scroll()
bind scrx = mouse_scroll_x()
# scripted input for automated UI tests (--drive / --scroll / --do)
if st_get("fake_on", 0.0) > 0.5 {
bind fr = st_get("frame", 0.0)
while st_get("dv_i", 0.0) < st_llen("dv_f") && st_lget("dv_f", st_get("dv_i", 0.0)) <= fr {
bind k = st_get("dv_i", 0.0)
st_set("fake_x", st_lget("dv_x", k))
st_set("fake_y", st_lget("dv_y", k))
st_set("fake_d", st_lget("dv_d", k))
st_set("fake_r", st_lget("dv_r", k))
st_set("dv_i", k + 1.0)
}
bind mx = st_get("fake_x", 0.0)
bind my = st_get("fake_y", 0.0)
bind dn = st_get("fake_d", 0.0)
bind rdn = st_get("fake_r", 0.0)
bind scr = 0.0
while st_get("sv_i", 0.0) < st_llen("sv_f") && st_lget("sv_f", st_get("sv_i", 0.0)) <= fr {
bind scr = scr + st_lget("sv_v", st_get("sv_i", 0.0))
st_set("sv_i", st_get("sv_i", 0.0) + 1.0)
}
while st_get("do_i", 0.0) < st_llen("do_f") && st_lget("do_f", st_get("do_i", 0.0)) <= fr {
do_action(st_lget("do_a", st_get("do_i", 0.0)))
st_set("do_i", st_get("do_i", 0.0) + 1.0)
}
}
bind prev = st_get("md", 0.0)
bind rprev = st_get("mrd", 0.0)
bind mprev = st_get("mmd", 0.0)
bind now = time_now()
st_set("wheel_used", 0.0)
st_set("bar_busy", 0.0)
st_set("mp", 0.0)
st_set("mr", 0.0)
st_set("mrp", 0.0)
st_set("mrr", 0.0)
st_set("mmp", 0.0)
st_set("dbl", 0.0)
if dn > 0.5 && prev < 0.5 {
st_set("mp", 1.0)
bind lt = st_get("lastclick_t", 0.0 - 10.0)
bind lx = st_get("lastclick_x", 0.0)
bind ly = st_get("lastclick_y", 0.0)
if now - lt < 0.4 && abs(mx - lx) < 5.0 && abs(my - ly) < 5.0 {
st_set("dbl", 1.0)
st_set("lastclick_t", 0.0 - 10.0)
} else {
st_set("lastclick_t", now)
st_set("lastclick_x", mx)
st_set("lastclick_y", my)
}
}
if dn < 0.5 && prev > 0.5 { st_set("mr", 1.0) }
if rdn > 0.5 && rprev < 0.5 { st_set("mrp", 1.0) }
if rdn < 0.5 && rprev > 0.5 { st_set("mrr", 1.0) }
if mdn > 0.5 && mprev < 0.5 { st_set("mmp", 1.0) }
st_set("pmx", st_get("mx", mx))
st_set("pmy", st_get("my", my))
st_set("md", dn)
st_set("mrd", rdn)
st_set("mmd", mdn)
st_set("mx", mx)
st_set("my", my)
st_set("scroll", scr)
st_set("scroll_x", scrx)
bind sh = 0.0
if key_down("lshift") || key_down("rshift") { bind sh = 1.0 }
bind ct = 0.0
if key_down("lctrl") || key_down("rctrl") { bind ct = 1.0 }
bind al = 0.0
if key_down("lalt") || key_down("ralt") { bind al = 1.0 }
st_set("shift", sh)
st_set("ctrl", ct)
st_set("alt", al)
st_set("dt", clampf(now - st_get("now", now), 0.001, 0.05))
st_set("now", now)
st_set("tip", "")
st_set("cursor_hand", 0.0)
}
fn u_end() {
if st_get("md", 0.0) < 0.5 { st_set("active", "") }
}
fn u_mx() { return st_get("mx", 0.0) }
fn u_my() { return st_get("my", 0.0) }
fn u_pmx() { return st_get("pmx", 0.0) }
fn u_pmy() { return st_get("pmy", 0.0) }
fn u_down() { return st_get("md", 0.0) > 0.5 }
fn u_pressed() { return st_get("mp", 0.0) > 0.5 }
fn u_released() { return st_get("mr", 0.0) > 0.5 }
fn u_rdown() { return st_get("mrd", 0.0) > 0.5 }
fn u_rpressed() { return st_get("mrp", 0.0) > 0.5 }
fn u_rreleased() { return st_get("mrr", 0.0) > 0.5 }
fn u_mdown() { return st_get("mmd", 0.0) > 0.5 }
fn u_mpressed() { return st_get("mmp", 0.0) > 0.5 }
fn u_double() { return st_get("dbl", 0.0) > 0.5 }
fn u_shift() { return st_get("shift", 0.0) > 0.5 }
fn u_ctrl() { return st_get("ctrl", 0.0) > 0.5 }
fn u_alt() { return st_get("alt", 0.0) > 0.5 }
fn u_scroll() { return st_get("scroll", 0.0) }
fn u_scroll_x() { return st_get("scroll_x", 0.0) }
# is the pointer inside a rect (and input not blocked by an overlay)?
fn u_hot(x, y, w, h) {
if st_get("blk", 0.0) > 0.5 { return false }
bind mx = st_get("mx", 0.0)
bind my = st_get("my", 0.0)
if mx < x || my < y || mx >= x + w || my >= y + h { return false }
# clipped regions hide widgets that are scrolled out of view
if st_get("clip_on", 0.0) > 0.5 {
if mx < st_get("clip_x0", 0.0) || mx >= st_get("clip_x1", 0.0) || my < st_get("clip_y0", 0.0) || my >= st_get("clip_y1", 0.0) { return false }
}
return true
}
# plain geometric test, ignores overlays (for canvases that own the drag)
fn u_in(x, y, w, h) {
bind mx = st_get("mx", 0.0)
bind my = st_get("my", 0.0)
return mx >= x && my >= y && mx < x + w && my < y + h
}
fn u_clip(x, y, w, h) {
px_clip(x, y, w, h)
st_set("clip_on", 1.0)
st_set("clip_x0", x)
st_set("clip_y0", y)
st_set("clip_x1", x + w)
st_set("clip_y1", y + h)
}
fn u_unclip() {
px_unclip()
st_set("clip_on", 0.0)
}
fn u_active() { return st_get("active", "") }
fn u_set_active(id) { st_set("active", id) }
fn u_tip(text) {
st_set("tip", text)
st_set("tip_t", st_get("now", 0.0))
}
# ── colour helpers ───────────────────────────────────────────────────────────
fn c_r(c) { return floor(c / 65536.0) }
fn c_g(c) { return floor(c / 256.0) % 256.0 }
fn c_b(c) { return c % 256.0 }
fn c_rgb(r, g, b) { return floor(clampf(r, 0.0, 255.0)) * 65536.0 + floor(clampf(g, 0.0, 255.0)) * 256.0 + floor(clampf(b, 0.0, 255.0)) }
fn c_mix(a, b, t) {
return c_rgb(c_r(a) + (c_r(b) - c_r(a)) * t, c_g(a) + (c_g(b) - c_g(a)) * t, c_b(a) + (c_b(b) - c_b(a)) * t)
}
# multiply brightness (egui-style tint)
fn c_tint(c, k) {
return c_rgb(min(255.0, c_r(c) * k), min(255.0, c_g(c) * k), min(255.0, c_b(c) * k))
}
fn c_pal(i) {
# ROYGBIV palette
bind k = floor(i) % 7.0
if k < 0.5 { return C_RED }
if k < 1.5 { return C_ORANGE }
if k < 2.5 { return C_YELLOW }
if k < 3.5 { return C_GREEN }
if k < 4.5 { return C_BLUE }
if k < 5.5 { return C_INDIGO }
return C_VIOLET
}
# "#rrggbb" <-> packed
fn c_hex(c) {
bind h = "0123456789abcdef"
bind out = "#"
bind parts = [c_r(c), c_g(c), c_b(c)]
bind i = 0.0
while i < 3.0 {
bind v = list_get(parts, i)
bind out = out + str_slice(h, floor(v / 16.0), 1.0) + str_slice(h, v % 16.0, 1.0)
bind i = i + 1.0
}
return out
}
fn c_from_hex(s) {
bind t = str_lower(str_trim(s))
if str_starts_with(t, "#") { bind t = str_slice(t, 1.0, str_len(t) - 1.0) }
if str_len(t) == 3.0 {
bind t = str_slice(t, 0.0, 1.0) + str_slice(t, 0.0, 1.0) + str_slice(t, 1.0, 1.0) + str_slice(t, 1.0, 1.0) + str_slice(t, 2.0, 1.0) + str_slice(t, 2.0, 1.0)
}
if str_len(t) < 6.0 { return 0.0 - 1.0 }
bind h = "0123456789abcdef"
bind v = 0.0
bind i = 0.0
while i < 6.0 {
bind d = str_find(h, str_slice(t, i, 1.0))
if d < 0.0 { return 0.0 - 1.0 }
bind v = v * 16.0 + d
bind i = i + 1.0
}
return v
}
# hsv (0..1) -> packed
fn c_hsv(h, s, v) {
bind hh = (h - floor(h)) * 6.0
bind i = floor(hh)
bind f = hh - i
bind p = v * (1.0 - s)
bind q = v * (1.0 - s * f)
bind t = v * (1.0 - s * (1.0 - f))
bind r = v
bind g = t
bind b = p
if i == 1.0 { bind r = q bind g = v bind b = p }
if i == 2.0 { bind r = p bind g = v bind b = t }
if i == 3.0 { bind r = p bind g = q bind b = v }
if i == 4.0 { bind r = t bind g = p bind b = v }
if i == 5.0 { bind r = v bind g = p bind b = q }
return c_rgb(r * 255.0, g * 255.0, b * 255.0)
}
fn sel(cond, a, b) {
if cond { return a }
return b
}
fn clampf(v, lo, hi) {
if v < lo { return lo }
if v > hi { return hi }
return v
}
fn fmt1(v) { return num_str(round(v * 10.0) / 10.0) }
fn fmt2(v) { return num_str(round(v * 100.0) / 100.0) }
fn fmt3(v) { return num_str(round(v * 1000.0) / 1000.0) }
fn fmt0(v) { return num_str(round(v)) }
# ── text ─────────────────────────────────────────────────────────────────────
fn g_text(x, y, size, c, s) { px_text(st_get("font", 0.0), x, y, size, c, s) }
fn g_text_w(size, s) { return font_width(st_get("font", 0.0), size, s) }
fn g_text_c(cx, y, size, c, s) { g_text(cx - g_text_w(size, s) * 0.5, y, size, c, s) }
fn g_text_r(rx, y, size, c, s) { g_text(rx - g_text_w(size, s), y, size, c, s) }
fn g_text_fit(x, y, maxw, size, c, s) {
g_text(x, y, size, c, px_fit(st_get("font", 0.0), size, maxw, s))
}
# word-wrapped text within maxw; returns the height used
fn g_text_wrap(x, y, maxw, size, c, s) {
bind f = st_get("font", 0.0)
bind words = str_split(s, " ")
bind line = ""
bind ly = y
bind lh = size + 4.0
bind i = 0.0
while i < len(words) {
bind wd = list_get(words, i)
bind trial = sel(line == "", wd, line + " " + wd)
if line != "" && font_width(f, size, trial) > maxw {
g_text(x, ly, size, c, px_fit(f, size, maxw, line))
bind ly = ly + lh
bind line = wd
} else {
bind line = trial
}
bind i = i + 1.0
}
if line != "" {
g_text(x, ly, size, c, px_fit(f, size, maxw, line))
bind ly = ly + lh
}
return ly - y
}
# ── shapes ───────────────────────────────────────────────────────────────────
fn g_rect(x, y, w, h, c) { px_rect(x, y, w, h, c) }
fn g_rect_a(x, y, w, h, c, a) { px_rect(x, y, w, h, c, a) }
fn g_frame(x, y, w, h, c) { px_outline(x, y, w, h, c) }
fn g_frame_a(x, y, w, h, c, a) { px_outline(x, y, w, h, c, a) }
fn g_rrect(x, y, w, h, r, c) { px_rrect(x, y, w, h, r, c) }
fn g_rrect_a(x, y, w, h, r, c, a) { px_rrect(x, y, w, h, r, c, a) }
fn g_rframe(x, y, w, h, r, c) { px_rrect_outline(x, y, w, h, r, c) }
fn g_line(x0, y0, x1, y1, c) { px_line(x0, y0, x1, y1, c) }
fn g_line_a(x0, y0, x1, y1, c, a) { px_line(x0, y0, x1, y1, c, a) }
fn g_disc(cx, cy, r, c) { px_disc(cx, cy, r, c) }
fn g_ring(cx, cy, r, c) { px_ring(cx, cy, r, c) }
fn g_tri(x0, y0, x1, y1, x2, y2, c) { p_tri(x0, y0, x1, y1, x2, y2, c, 1.0) }
fn g_poly(pts, c) { p_poly(pts, c, 1.0) }
fn g_poly_a(pts, c, a) { p_poly(pts, c, a) }
# thick line as a quad
fn g_line_w(x0, y0, x1, y1, w, c) {
if w <= 1.5 { px_line(x0, y0, x1, y1, c) return 0.0 }
bind dx = x1 - x0
bind dy = y1 - y0
bind l = sqrt(dx * dx + dy * dy)
if l < 0.001 { px_disc(x0, y0, w * 0.5, c) return 0.0 }
bind nx = 0.0 - dy / l * w * 0.5
bind ny = dx / l * w * 0.5
p_poly([x0 + nx, y0 + ny, x1 + nx, y1 + ny, x1 - nx, y1 - ny, x0 - nx, y0 - ny], c, 1.0)
px_disc(x0, y0, w * 0.5, c)
px_disc(x1, y1, w * 0.5, c)
}
fn g_dash(x0, y0, x1, y1, c, seg) {
bind dx = x1 - x0
bind dy = y1 - y0
bind l = sqrt(dx * dx + dy * dy)
if l < 0.5 { return 0.0 }
bind n = max(1.0, floor(l / seg))
bind i = 0.0
while i < n {
if i % 2.0 < 0.5 {
bind t0 = i / n
bind t1 = (i + 1.0) / n
px_line(x0 + dx * t0, y0 + dy * t0, x0 + dx * t1, y0 + dy * t1, c)
}
bind i = i + 1.0
}
}
# dashed rectangle (box select)
fn g_dash_rect(x, y, w, h, c) {
g_dash(x, y, x + w, y, c, 4.0)
g_dash(x + w, y, x + w, y + h, c, 4.0)
g_dash(x + w, y + h, x, y + h, c, 4.0)
g_dash(x, y + h, x, y, c, 4.0)
}
# ── widgets ──────────────────────────────────────────────────────────────────
# push button: returns true on click (release inside)
fn u_button(id, x, y, w, h, label, fill) {
bind hot = u_hot(x, y, w, h)
bind act = u_active() == id
if hot && u_pressed() { u_set_active(id) }
bind col = ds_col("ink")
if fill > 0.5 { bind col = sel(c_r(fill) > c_b(fill) + 30.0, ds_col("asm"), ds_col("call")) }
px_rect(x, y, w, h, c_mix(C_BG_PANEL, 0.0, 0.35))
if fill > 0.5 { ds_hatch(x + 1.0, y + 1.0, w - 2.0, h - 2.0, col, 45.0, 5.0, 0.22) }
if hot { ds_hatch(x + 1.0, y + 1.0, w - 2.0, h - 2.0, col, 45.0, 4.0, 0.3) }
if act && hot { px_rect(x, y, w, h, col, 0.25) }
px_outline(x, y, w, h, col, sel(hot || fill > 0.5, 0.95, 0.6))
if hot || fill > 0.5 { ds_brackets(x, y, w, h, min(5.0, h * 0.3), col) }
g_text_c(x + w * 0.5, y + (h - 12.0) * 0.5 - 1.0, 12.0, C_TEXT_STRONG, label)
if hot { st_set("cursor_hand", 1.0) }
return act && hot && u_released()
}
# button with a vector icon instead of text; on = toggled look
fn u_icon_button(id, x, y, w, h, icon, size, tint, on) {
bind hot = u_hot(x, y, w, h)
bind act = u_active() == id
if hot && u_pressed() { u_set_active(id) }
bind bg = C_BTN
if on { bind bg = C_SEL }
if hot { bind bg = c_tint(bg, 1.15) }
if act && hot { bind bg = c_tint(bg, 0.85) }
g_rrect(x, y, w, h, 3.0, bg)
icon_draw(icon, x + w * 0.5, y + h * 0.5, size, tint)
return act && hot && u_released()
}
# tool button: icon + short label, hatched when on. returns clicked
fn u_tool(id, x, y, w, h, icon, label, on, col, tip) {
bind hot = u_hot(x, y, w, h)
bind act = u_active() == id
if hot && u_pressed() { u_set_active(id) }
bind kc = sel(col > 0.5, col, ds_col("call"))
px_rect(x, y, w, h, c_mix(C_BG_PANEL, 0.0, 0.3))
if on { ds_hatch(x + 1.0, y + 1.0, w - 2.0, h - 2.0, kc, 45.0, 4.0, 0.35) }
else if hot { ds_hatch(x + 1.0, y + 1.0, w - 2.0, h - 2.0, ds_col("ink"), 45.0, 4.0, 0.25) }
px_outline(x, y, w, h, sel(on, kc, ds_col("ink")), sel(on || hot, 0.95, 0.5))
if on { ds_brackets(x, y, w, h, 5.0, kc) }
if label == "" {
icon_draw(icon, x + w * 0.5, y + h * 0.5, min(h - 8.0, 16.0), sel(on, C_TEXT_STRONG, C_TEXT))
} else {
icon_draw(icon, x + 12.0, y + h * 0.5, 13.0, sel(on, C_TEXT_STRONG, C_TEXT))
g_text_fit(x + 22.0, y + (h - 11.0) * 0.5 - 1.0, w - 26.0, 11.0, sel(on, C_TEXT_STRONG, C_TEXT), label)
}
if hot && tip != "" { u_tip(tip) }
return act && hot && u_released()
}
# flat chip button: fill when on
fn u_chip(id, x, y, w, h, label, on, col) {
bind hot = u_hot(x, y, w, h)
bind act = u_active() == id
if hot && u_pressed() { u_set_active(id) }
px_rect(x, y, w, h, c_mix(C_BG_PANEL, 0.0, 0.3))
if on { ds_cross(x + 1.0, y + 1.0, w - 2.0, h - 2.0, col, 3.0, 0.55) }
else if hot { ds_hatch(x + 1.0, y + 1.0, w - 2.0, h - 2.0, col, 45.0, 4.0, 0.3) }
px_outline(x, y, w, h, col, sel(on || hot, 0.95, 0.55))
bind tc = C_TEXT
if on { bind tc = 16777215.0 }
g_text_c(x + w * 0.5, y + (h - 11.0) * 0.5 - 1.0, 11.0, tc, label)
return act && hot && u_released()
}
fn u_check(id, x, y, label, on) {
bind w = 20.0 + g_text_w(12.0, label)
bind hot = u_hot(x, y, w, 16.0)
bind act = u_active() == id
if hot && u_pressed() { u_set_active(id) }
bind col = sel(on, ds_col("ctl"), ds_col("ink"))
px_outline(x, y + 1.0, 14.0, 14.0, col, sel(on || hot, 0.95, 0.6))
ds_brackets(x, y + 1.0, 14.0, 14.0, 4.0, col)
if on { ds_cross(x + 2.0, y + 3.0, 10.0, 10.0, col, 3.0, 0.75) }
g_text(x + 20.0, y + 1.0, 12.0, C_TEXT, label)
return act && hot && u_released()
}
# selectable text/tab. returns true on click
fn u_tab(id, x, y, w, h, label, on, col) {
bind hot = u_hot(x, y, w, h)
bind act = u_active() == id
if hot && u_pressed() { u_set_active(id) }
bind kc = sel(col > 0.5, col, ds_col("call"))
if on {
px_rect(x, y, w, h, c_mix(C_BG_PANEL, 0.0, 0.3))
ds_hatch(x + 1.0, y + 1.0, w - 2.0, h - 2.0, kc, 45.0, 5.0, 0.2)
px_outline(x, y, w, h, kc, 0.9)
ds_brackets(x, y, w, h, 5.0, kc)
} else if hot {
px_outline(x, y, w, h, ds_col("ink"), 0.7)
}
bind tc = C_TEXT
if on { bind tc = C_TEXT_STRONG }
g_text_c(x + w * 0.5, y + (h - 12.0) * 0.5 - 1.0, 12.0, tc, label)
return act && hot && u_released()
}
# horizontal slider. returns new value or -999 if unchanged
fn u_hslider(id, x, y, w, h, v, lo, hi, col) {
bind hot = u_hot(x, y, w, h)
if hot && u_pressed() {
u_set_active(id)
st_set("sl_x0", u_mx())
st_set("sl_v0", v)
}
bind out = 0.0 - 999.0
if u_active() == id && u_down() {
bind t = clampf((u_mx() - x - 4.0) / (w - 8.0), 0.0, 1.0)
if u_shift() {
bind t = clampf((st_get("sl_v0", v) - lo) / (hi - lo) + (u_mx() - st_get("sl_x0", u_mx())) / (w * 8.0), 0.0, 1.0)
}
bind out = lo + t * (hi - lo)
}
if hot && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 && st_get("blk", 0.0) < 0.5 {
bind out = clampf(v + u_scroll() * (hi - lo) * 0.02, lo, hi)
st_set("wheel_used", 1.0)
}
bind cur = sel(out > 0.0 - 900.0, out, v)
bind t = clampf((cur - lo) / (hi - lo), 0.0, 1.0)
bind ty = y + h * 0.5
px_outline(x, ty - 3.0, w, 6.0, ds_col("ink"), 0.6)
ds_hatch(x + 1.0, ty - 2.0, max(1.0, (w - 2.0) * t), 4.0, col, 45.0, 3.0, sel(hot || u_active() == id, 0.95, 0.65))
bind kx = x + 4.0 + (w - 8.0) * t
px_rect(kx - 3.0, ty - 6.0, 6.0, 12.0, c_mix(C_BG_PANEL, 0.0, 0.3))
px_outline(kx - 3.0, ty - 6.0, 6.0, 12.0, col, 1.0)
return out
}
# labelled slider row: label left, value right. returns new value or -999
fn u_param(id, x, y, w, label, v, lo, hi, col, decimals) {
g_text(x, y, 11.0, C_TEXT_DIM, label)
bind txt = sel(decimals < 0.5, fmt0(v), sel(decimals < 1.5, fmt1(v), sel(decimals < 2.5, fmt2(v), fmt3(v))))
g_text_r(x + w, y, 11.0, C_TEXT_STRONG, txt)
return u_hslider(id, x, y + 13.0, w, 14.0, v, lo, hi, col)
}
# horizontal draggable number field. returns new value or -999
fn u_dragval(id, x, y, w, h, v, lo, hi, speed, label) {
bind hot = u_hot(x, y, w, h)
if hot && u_pressed() {
u_set_active(id)
st_set("ddv_x", u_mx())
st_set("ddv_v", v)
}
bind out = 0.0 - 999.0
if u_active() == id && u_down() {
bind k = speed
if u_shift() { bind k = speed * 0.1 }
bind out = clampf(st_get("ddv_v", v) + (u_mx() - st_get("ddv_x", 0.0)) * k, lo, hi)
}
if hot && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 { bind out = clampf(v + u_scroll() * speed * 4.0, lo, hi) st_set("wheel_used", 1.0) }
px_rect(x, y, w, h, c_mix(C_BG_PANEL, 0.0, 0.3))
px_outline(x, y, w, h, sel(hot || u_active() == id, ds_col("call"), ds_col("ink")), sel(hot, 0.9, 0.5))
if hot { ds_hatch(x + 1.0, y + 1.0, w - 2.0, h - 2.0, ds_col("ink"), 45.0, 5.0, 0.2) }
g_text_c(x + w * 0.5, y + (h - 11.0) * 0.5 - 1.0, 11.0, C_TEXT_STRONG, label)
return out
}
# xyz triple of drag fields. returns [changedIndex, value] or [-1, 0]
fn u_vec3(id, x, y, w, vx, vy, vz, lo, hi, speed) {
bind cw = floor((w - 8.0) / 3.0)
bind cols = [C_RED, C_GREEN, C_BLUE]
bind vals = [vx, vy, vz]
bind out = [0.0 - 1.0, 0.0]
bind i = 0.0
while i < 3.0 {
bind cx = x + i * (cw + 4.0)
g_rect(cx, y, 2.0, 20.0, list_get(cols, i))
bind nv = u_dragval(id + num_str(i), cx + 3.0, y, cw - 3.0, 20.0, list_get(vals, i), lo, hi, speed, fmt2(list_get(vals, i)))
if u_has(nv) { bind out = [i, nv] }
bind i = i + 1.0
}
return out
}
fn u_has(v) { return v > 0.0 - 900.0 }
# vertical scrollbar + wheel for a scroll region. returns new scroll offset.
fn u_scroll_y(id, x, y, w, h, content, off) {
bind maxoff = max(0.0, content - h)
bind o = clampf(off, 0.0, maxoff)
if maxoff > 0.0 && u_hot(x, y, w, h) && u_scroll() != 0.0 && !u_ctrl() && !u_shift() {
bind o = clampf(o - u_scroll() * 24.0, 0.0, maxoff)
st_set("wheel_used", 1.0)
}
if maxoff > 0.0 {
bind bx = x + w - 10.0
bind th = max(28.0, h * h / content)
bind ty = y + (h - th) * (o / maxoff)
bind hot = u_hot(bx - 3.0, y, 14.0, h)
if hot && u_pressed() {
u_set_active(id)
st_set("sb_grab", sel(u_my() >= ty && u_my() <= ty + th, u_my() - ty, th * 0.5))
}
if u_active() == id && u_down() {
bind o = clampf((u_my() - st_get("sb_grab", 0.0) - y) / (h - th), 0.0, 1.0) * maxoff
bind ty = y + (h - th) * (o / maxoff)
st_set("bar_busy", 1.0)
}
if hot { st_set("bar_busy", 1.0) }
g_rect_a(bx, y + 1.0, 8.0, h - 2.0, 0, 0.35)
g_frame(bx, y + 1.0, 8.0, h - 2.0, C_LINE_SOFT)
px_rect(bx + 1.0, ty + 1.0, 6.0, th - 2.0, sel(hot || u_active() == id, C_TEXT_DIM, C_BTN_HOV))
}
return o
}
# thin separator lines
fn u_hsep(x, y, w) { g_rect(x, y, w, 1.0, C_LINE) }
fn u_vsep(x, y, h) { g_rect(x, y, 1.0, h, C_LINE) }
# section header inside a panel
fn u_header(x, y, w, label, col) {
ds_word(x, y, 11.0, str_upper(label), col, false)
bind tw = ds_word_w(11.0, label)
g_rect(x + tw + 6.0, y + 5.0, max(0.0, w - tw - 6.0), 1.0, C_LINE)
}
# a list row (outliner / layers). returns 1 click, 2 double click, 0 none
fn u_row(id, x, y, w, h, label, on, col, icon) {
bind hot = u_hot(x, y, w, h)
if on { px_rect(x, y, w, h, C_SEL_ROW) ds_hatch(x + 1.0, y + 1.0, w - 2.0, h - 2.0, col, 45.0, 6.0, 0.15) }
else if hot { px_rect(x, y, w, h, C_HOVER_ROW) }
if on { g_rect(x, y, 2.0, h, col) }
bind tx = x + 8.0
if icon != "" { icon_draw(icon, x + 12.0, y + h * 0.5, 11.0, col) bind tx = x + 24.0 }
g_text_fit(tx, y + (h - 12.0) * 0.5 - 1.0, w - (tx - x) - 6.0, 12.0, sel(on, C_TEXT_STRONG, C_TEXT), label)
if hot && u_pressed() {
if u_double() { return 2.0 }
return 1.0
}
return 0.0
}
# colour swatch button. returns clicked
fn u_swatch(id, x, y, s, c, on) {
bind hot = u_hot(x, y, s, s)
px_rect(x, y, s, s, c)
px_outline(x, y, s, s, sel(on, C_TEXT_STRONG, sel(hot, C_TEXT, C_LINE)), 1.0)
if on { ds_brackets(x - 1.0, y - 1.0, s + 2.0, s + 2.0, 4.0, C_TEXT_STRONG) }
return hot && u_pressed()
}
# ── dropdown (combo box). returns the newly chosen index, or -1 when nothing changed ──
fn u_combo(id, x, y, w, h, names, cur) {
bind out = 0.0 - 1.0
bind res = st_get("cb_res", "")
if res != "" {
bind parts = str_split(res, ":")
if list_get(parts, 0) == id {
bind out = to_number(list_get(parts, 1))
st_set("cb_res", "")
}
}
bind open = st_get("cb_open", "") == id
bind hot = u_hot(x, y, w, h)
px_rect(x, y, w, h, c_mix(C_BG_PANEL, 0.0, 0.3))
px_outline(x, y, w, h, sel(hot || open, ds_col("call"), ds_col("ink")), sel(hot || open, 0.9, 0.5))
if len(names) > 0.5 { g_text_fit(x + 8.0, y + (h - 12.0) * 0.5 - 1.0, w - 26.0, 12.0, C_TEXT_STRONG, list_get(names, clampf(cur, 0.0, len(names) - 1.0))) }
icon_draw("chev_d", x + w - 12.0, y + h * 0.5, 8.0, C_TEXT_DIM)
if hot && u_pressed() && len(names) > 0.5 {
st_set("cb_open", id)
st_set("cb_x", x)
st_set("cb_top", y)
st_set("cb_hgt", h)
st_set("cb_off", 0.0)
st_set("cb_drag", 0.0)
st_set("cb_y", y + h + 2.0)
st_set("cb_w", w)
st_set("cb_items", names)
st_set("cb_cur", cur)
st_set("cb_eat", 1.0)
}
return out
}
# the open dropdown: a scrollable list that stays on screen and ignores the click that opened it
fn combo_overlay() {
bind id = st_get("cb_open", "")
if id == "" { return 0.0 }
st_set("blk", 0.0)
bind names = st_get("cb_items", [])
bind n = len(names)
bind cur = st_get("cb_cur", 0.0 - 1.0)
bind ih = 22.0
bind H = get_height()
bind W = get_width()
bind top = st_get("cb_top", 0.0)
bind chh = st_get("cb_hgt", 24.0)
bind below = H - (top + chh + 2.0) - 8.0
bind above = top - 2.0 - 8.0
bind fit_rows = floor((below - 8.0) / ih)
bind rows = min(n, fit_rows)
bind up = false
if rows < n && above > below {
bind rows = min(n, floor((above - 8.0) / ih))
bind up = true
}
bind rows = max(3.0, rows)
bind h = rows * ih + 8.0
bind w = st_get("cb_w", 120.0)
bind i = 0.0
while i < n {
bind w = max(w, g_text_w(12.0, list_get(names, i)) + 44.0)
bind i = i + 1.0
}
bind w = min(w, W - 16.0)
bind x = min(st_get("cb_x", 0.0), W - w - 6.0)
bind y = sel(up, top - 2.0 - h, top + chh + 2.0)
bind sbw = sel(n > rows, 10.0, 0.0)
bind inside = u_mx() >= x && u_mx() < x + w && u_my() >= y && u_my() < y + h
bind off = clampf(st_get("cb_off", 0.0), 0.0, max(0.0, n - rows))
if inside && u_scroll() != 0.0 { bind off = clampf(off - u_scroll() * 3.0, 0.0, max(0.0, n - rows)) st_set("wheel_used", 1.0) }
if n > rows {
bind thumb = max(24.0, (h - 8.0) * rows / n)
bind span = h - 8.0 - thumb
if inside && u_mx() >= x + w - sbw - 2.0 && u_pressed() && st_get("cb_eat", 0.0) < 0.5 { st_set("cb_drag", 1.0) }
if st_get("cb_drag", 0.0) > 0.5 {
if u_down() { bind off = clampf((u_my() - y - 4.0 - thumb * 0.5) / max(1.0, span) * (n - rows), 0.0, n - rows) } else { st_set("cb_drag", 0.0) }
}
}
st_set("cb_off", off)
px_rect(x + 2.0, y + 3.0, w, h, 0, 0.35)
px_rect(x, y, w, h, C_POPUP)
px_outline(x, y, w, h, C_POPUP_LINE)
ds_brackets(x, y, w, h, 6.0, ds_col("call"))
u_clip(x, y + 2.0, w, h - 4.0)
bind pick = 0.0 - 1.0
bind r = 0.0
while r < rows {
bind idx = floor(off) + r
if idx < n {
bind ry = y + 4.0 + r * ih
bind hot = u_hot(x + 2.0, ry, w - 4.0 - sbw, ih) && st_get("cb_drag", 0.0) < 0.5
if hot { px_rect(x + 3.0, ry, w - 6.0 - sbw, ih, C_HOVER_ROW) }
if idx == cur { px_rect(x + 6.0, ry + 6.0, 10.0, 10.0, C_BLUE) }
g_text_fit(x + 24.0, ry + 4.0, w - 32.0 - sbw, 12.0, sel(idx == cur, C_TEXT_STRONG, C_TEXT), list_get(names, idx))
if hot && u_pressed() && st_get("cb_eat", 0.0) < 0.5 { bind pick = idx }
}
bind r = r + 1.0
}
u_unclip()
if n > rows {
bind thumb = max(24.0, (h - 8.0) * rows / n)
bind span = h - 8.0 - thumb
bind ty = y + 4.0 + sel(n - rows > 0.0, off / (n - rows), 0.0) * span
px_rect(x + w - sbw - 2.0, y + 4.0, sbw - 2.0, h - 8.0, 0, 0.35)
px_rect(x + w - sbw - 2.0, ty, sbw - 2.0, thumb, sel(st_get("cb_drag", 0.0) > 0.5, C_TEXT_DIM, C_BTN_HOV))
}
if pick >= 0.0 {
st_set("cb_res", id + ":" + num_str(pick))
st_set("cb_open", "")
} else if (u_pressed() && st_get("cb_eat", 0.0) < 0.5 && !inside) || key_pressed("escape") { st_set("cb_open", "") }
st_set("cb_eat", 0.0)
}
# ── text input ───────────────────────────────────────────────────────────────
fn u_textfield(id, x, y, w, h, value) {
bind focused = st_get("focus", "") == id
bind hot = u_hot(x, y, w, h)
if focused && u_pressed() && !hot && st_get("blk", 0.0) < 0.5 {
st_set("focus", "")
bind focused = false
}
if hot && u_pressed() && !focused {
st_set("focus", id)
p_text_set(value)
bind focused = true
}
bind v = value
if focused { bind v = p_text_poll() }
px_rect(x, y, w, h, C_BG_DEEP)
px_outline(x, y, w, h, sel(focused, ds_col("call"), ds_col("ink")), sel(focused, 0.95, 0.5))
if focused { ds_brackets(x, y, w, h, 4.0, ds_col("call")) }
g_text_fit(x + 6.0, y + (h - 12.0) * 0.5 - 1.0, w - 12.0, 12.0, C_TEXT_STRONG, v)
if focused {
bind cx = x + 6.0 + g_text_w(12.0, v)
if floor(st_get("now", 0.0) * 2.0) % 2.0 < 0.5 { g_rect(min(cx, x + w - 6.0), y + 4.0, 1.0, h - 8.0, C_TEXT_STRONG) }
if key_pressed("enter") || key_pressed("tab") { st_set("focus", "") }
}
return v
}
# numeric text field: returns the new number or -999 when unchanged / still being edited
fn u_numfield(id, x, y, w, h, v) {
bind focused = st_get("focus", "") == id
bind shown = sel(focused, st_get("nf_txt", fmt3(v)), fmt3(v))
bind hot = u_hot(x, y, w, h)
if hot && u_pressed() && !focused { st_set("nf_txt", fmt3(v)) }
bind t = u_textfield(id, x, y, w, h, shown)
if st_get("focus", "") == id {
st_set("nf_txt", t)
return 0.0 - 999.0
}
if focused {
bind nv = to_number(str_trim(t))
if str_trim(t) != "" && abs(nv - v) > 0.00001 { return nv }
}
return 0.0 - 999.0
}