灵符lingfu

lingdev

↓ 0 下载

LingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).

Windows · x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
关键词 game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev
许可证Apache-2.0 OR MIT
作者ling-lin
README代码 / 文件版本依赖关系图依赖此包的项目安全构建设置

代码 / 文件 (136)

ling/core.ling
# core.ling - input frame, hit-testing, colour and text helpers, the widget toolkit.
# All shared state lives in the native store (st_get / st_set) so plain functions can reach it.
# Colours are packed numbers r*65536 + g*256 + b. Every widget is drawn each frame and
# returns its state; `blk` > 0 blocks input to everything below an overlay.

# ── 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()
    # scripted input for screenshots / UI tests (--drive "frame,x,y,down;...")
    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("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 scr = 0.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("mmp", 0.0)
    st_set("dbl", 0.0)
    if dn > 0.5 && prev < 0.5 {
        st_set("mp", 1.0)
        st_set("press_x", mx)
        st_set("press_y", my)
        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 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)
    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_dx() { return st_get("mx", 0.0) - st_get("pmx", 0.0) }
fn u_dy() { return st_get("my", 0.0) - st_get("pmy", 0.0) }
fn u_down() { return st_get("md", 0.0) > 0.5 }
fn u_rdown() { return st_get("mrd", 0.0) > 0.5 }
fn u_mdown() { return st_get("mmd", 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_rpressed() { return st_get("mrp", 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_dt() { return st_get("dt", 0.016) }
fn u_now() { return st_get("now", 0.0) }

# pointer inside a rect, input not blocked by an overlay, and not scrolled out of a clip region
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 }
    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
}
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))
}

# ── small helpers ────────────────────────────────────────────────────────────
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 lerpf(a, b, t) { return a + (b - a) * t }
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 pad2(v) {
    if v < 10.0 { return "0" + num_str(floor(v)) }
    return num_str(floor(v))
}
fn bool_num(b) {
    if b { return 1.0 }
    return 0.0
}
# mm:ss.ff time readout
fn fmt_time(secs) {
    bind m = floor(secs / 60.0)
    bind s = secs - m * 60.0
    bind f = floor((s - floor(s)) * 100.0)
    return pad2(m) + ":" + pad2(floor(s)) + "." + pad2(f)
}

# ── 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)
}
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))
}
# "r,g,b" text <-> packed
fn c_parse(s) {
    bind p = str_split(s, ",")
    if len(p) < 3.0 { return to_number(s) }
    return c_rgb(to_number(list_get(p, 0)), to_number(list_get(p, 1)), to_number(list_get(p, 2)))
}
fn c_text(c) { return num_str(c_r(c)) + "," + num_str(c_g(c)) + "," + num_str(c_b(c)) }

# ── 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))
}
fn g_fit(size, maxw, s) { return px_fit(st_get("font", 0.0), size, maxw, s) }
# word-wrapped text; 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
}
fn g_text_outlined(x, y, size, c, s) {
    g_text(x - 1.0, y, size, 0.0, s)
    g_text(x + 1.0, y, size, 0.0, s)
    g_text(x, y - 1.0, size, 0.0, s)
    g_text(x, y + 1.0, size, 0.0, s)
    g_text(x, y, size, c, s)
}

# ── 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_disc_a(cx, cy, r, c, a) { px_disc(cx, cy, r, c, a) }
fn g_ring(cx, cy, r, c) { px_ring(cx, cy, r, c) }
fn g_hsep(x, y, w) { g_rect(x, y, w, 1.0, C_LINE) }
fn g_vsep(x, y, h) { g_rect(x, y, 1.0, h, C_LINE) }
fn u_hsep(x, y, w) { g_hsep(x, y, w) }
fn u_vsep(x, y, h) { g_vsep(x, y, h) }

# filled triangle from horizontal spans (works on every runtime: only px_hline)
fn g_tri_a(x0, y0, x1, y1, x2, y2, c, a) {
    bind ay = y0
    bind by = y1
    bind cy = y2
    bind ax = x0
    bind bx = x1
    bind cx = x2
    if by < ay { bind t = ay  bind ay = by  bind by = t  bind t = ax  bind ax = bx  bind bx = t }
    if cy < ay { bind t = ay  bind ay = cy  bind cy = t  bind t = ax  bind ax = cx  bind cx = t }
    if cy < by { bind t = by  bind by = cy  bind cy = t  bind t = bx  bind bx = cx  bind cx = t }
    bind y = floor(ay)
    bind yend = ceil(cy)
    if yend - y > 4000.0 { return 0.0 }
    while y <= yend {
        bind sy = y + 0.5
        if sy >= ay && sy <= cy {
            bind xa = ax
            if cy - ay > 0.0001 { bind xa = ax + (cx - ax) * (sy - ay) / (cy - ay) }
            bind xb = ax
            if sy < by {
                if by - ay > 0.0001 { bind xb = ax + (bx - ax) * (sy - ay) / (by - ay) }
            } else {
                bind xb = bx
                if cy - by > 0.0001 { bind xb = bx + (cx - bx) * (sy - by) / (cy - by) }
            }
            bind lo = min(xa, xb)
            bind hi = max(xa, xb)
            px_hline(round(lo), y, max(1.0, round(hi) - round(lo)), c, a)
        }
        bind y = y + 1.0
    }
    return 0.0
}
fn g_tri(x0, y0, x1, y1, x2, y2, c) { g_tri_a(x0, y0, x1, y1, x2, y2, c, 1.0) }
# diamond (keyframes)
fn g_diamond(cx, cy, r, c) {
    g_tri(cx - r, cy, cx, cy - r, cx + r, cy, c)
    g_tri(cx - r, cy, cx, cy + r, cx + r, cy, c)
}

# ── widgets ──────────────────────────────────────────────────────────────────
# push button, click = release inside. fill > 0 picks the key colour (blue, or red for a red fill)
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, g_fit(12.0, w - 8.0, label))
    if hot { st_set("cursor_hand", 1.0) }
    return act && hot && u_released()
}

# button with a vector icon; 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()
}

# flat chip (toggle look)
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 = C_WHITE }
    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 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 when 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
    g_rrect(x, ty - 2.0, w, 4.0, 2.0, C_BTN_ACT)
    g_rrect_a(x, ty - 2.0, max(4.0, w * t), 4.0, 2.0, col, sel(hot || u_active() == id, 0.9, 0.55))
    g_disc(x + 4.0 + (w - 8.0) * t, ty, sel(hot || u_active() == id, 6.0, 5.0), c_tint(col, 1.25))
    return out
}

# draggable number field (drag horizontally, Shift = fine, wheel, double-click to type). 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)
    bind out = 0.0 - 999.0
    bind editing = st_get("focus", "") == id
    if editing {
        bind t = u_textfield(id, x, y, w, h, st_get("dv_text", ""))
        st_set("dv_text", t)
        if key_pressed("enter") || (u_pressed() && !hot) {
            bind nv = to_number(str_trim(t))
            if str_trim(t) != "" { bind out = clampf(nv, lo, hi) }
            if st_get("focus", "") == id { st_set("focus", "") }
        }
        if key_pressed("escape") { st_set("focus", "") }
        return out
    }
    if hot && u_double() && st_get("blk", 0.0) < 0.5 {
        st_set("focus", id)
        st_set("dv_text", num_str(round(v * 1000.0) / 1000.0))
        text_set(st_get("dv_text", ""))
        return out
    }
    if hot && u_pressed() {
        u_set_active(id)
        st_set("ddv_x", u_mx())
        st_set("ddv_v", v)
    }
    if u_active() == id && u_down() {
        bind k = speed
        if u_shift() { bind k = speed * 0.1 }
        bind nv = clampf(st_get("ddv_v", v) + (u_mx() - st_get("ddv_x", 0.0)) * k, lo, hi)
        if abs(nv - v) > 0.0000001 { bind out = nv }
    }
    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)
    }
    g_rrect(x, y, w, h, 3.0, sel(hot || u_active() == id, C_BTN_HOV, C_BTN))
    g_text_c(x + w * 0.5, y + (h - 12.0) * 0.5 - 1.0, 12.0, C_TEXT_STRONG, g_fit(12.0, w - 6.0, label))
    return out
}
fn u_has(v) { return v > 0.0 - 900.0 }

# text input field. returns the (possibly edited) value
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)
        text_set(value)
        bind focused = true
    }
    bind v = value
    if focused { bind v = text_poll() }
    g_rrect(x, y, w, h, 3.0, C_BG_DEEP)
    g_rframe(x, y, w, h, 3.0, sel(focused, C_BLUE, C_LINE))
    bind shown = v
    bind tw = g_text_w(12.0, shown)
    bind tx = x + 6.0
    if focused && tw > w - 12.0 { bind tx = x + w - 6.0 - tw }
    ds_clip_to(x + 2.0, y, w - 4.0, h)
    g_text(tx, y + (h - 12.0) * 0.5 - 1.0, 12.0, C_TEXT_STRONG, shown)
    ds_clip_end()
    if focused {
        bind cx = min(tx + tw, x + w - 6.0)
        if floor(st_get("now", 0.0) * 2.0) % 2.0 < 0.5 { g_rect(cx, y + 4.0, 1.0, h - 8.0, C_TEXT_STRONG) }
    }
    return v
}

# vertical scroll region: wheel + bar. returns the new 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() && st_get("wheel_used", 0.0) < 0.5 {
        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, 0.35)
        g_frame(bx, y + 1.0, 8.0, h - 2.0, C_LINE_SOFT)
        g_rrect(bx + 1.0, ty + 1.0, 6.0, th - 2.0, 3.0, sel(hot || u_active() == id, C_TEXT_DIM, C_BTN_HOV))
    }
    return o
}
# horizontal bar that only draws and reports
fn u_bar_h(id, x, y, w, view, content, off) {
    bind maxoff = max(0.0, content - view)
    if maxoff <= 0.0 {
        g_rect_a(x, y, w, 10.0, 0.0, 0.25)
        g_frame(x, y, w, 10.0, C_LINE_SOFT)
        return 0.0
    }
    bind o = clampf(off, 0.0, maxoff)
    bind th = max(28.0, w * view / content)
    bind span = max(1.0, w - th)
    bind tx = x + span * (o / maxoff)
    bind hot = u_hot(x, y - 2.0, w, 14.0)
    if hot && u_pressed() {
        u_set_active(id)
        st_set("sb_grab", sel(u_mx() >= tx && u_mx() <= tx + th, u_mx() - tx, th * 0.5))
    }
    if u_active() == id && u_down() {
        bind o = clampf((u_mx() - st_get("sb_grab", 0.0) - x) / span, 0.0, 1.0) * maxoff
        bind tx = x + span * (o / maxoff)
        st_set("bar_busy", 1.0)
    }
    if hot { st_set("bar_busy", 1.0) }
    g_rect_a(x, y, w, 10.0, 0.0, 0.45)
    g_frame(x, y, w, 10.0, C_LINE_SOFT)
    g_rrect(tx + 1.0, y + 1.0, th - 2.0, 8.0, 3.0, sel(hot || u_active() == id, C_TEXT_DIM, C_BTN_HOV))
    return o
}

# dropdown. returns the newly chosen index, or -1
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)
    g_rrect(x, y, w, h, 3.0, sel(hot || open, C_BTN_HOV, C_BTN))
    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_w", w)
        st_set("cb_items", names)
        st_set("cb_cur", cur)
        st_set("cb_eat", 1.0)
    }
    return out
}

# the open dropdown list; call after everything else each frame
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 - 10.0
    bind rows = min(n, max(1.0, floor((below - 8.0) / ih)))
    bind up = false
    if rows < n && above > below {
        bind rows = min(n, floor((above - 8.0) / ih))
        bind up = true
    }
    bind rows = max(1.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 inside = u_in(x, y, w, 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)
    }
    st_set("cb_off", off)
    px_rrect(x + 2.0, y + 3.0, w, h, 6.0, 0.0, 0.35)
    g_rrect(x, y, w, h, 5.0, C_POPUP)
    g_rframe(x, y, w, h, 5.0, C_POPUP_LINE)
    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, ih)
            if hot { g_rrect_a(x + 3.0, ry, w - 6.0, ih, 3.0, C_BLUE, 0.30) }
            if idx == cur { g_rrect(x + 6.0, ry + 6.0, 10.0, 10.0, 2.0, C_BLUE) }
            g_text_fit(x + 24.0, ry + 4.0, w - 32.0, 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
        g_rrect_a(x + w - 8.0, y + 4.0, 6.0, h - 8.0, 3.0, 0.0, 0.35)
        g_rrect(x + w - 8.0, ty, 6.0, thumb, 3.0, 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)
}

# a labelled row header inside panels
fn u_section(x, y, w, label, col) {
    ds_word(x, y, 9.0, label, col, false)
    bind lw = ds_word_w(9.0, label)
    px_hline(x + lw + 8.0, y + 4.0, max(0.0, w - lw - 8.0), ds_col("ink"), 0.6)
}
# key/value readout line
fn u_kv(x, y, w, k, v) {
    g_text(x, y, 11.0, C_TEXT_DIM, k)
    g_text_r(x + w, y, 11.0, C_TEXT, g_fit(11.0, w * 0.6, v))
}

← 返回索引