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).
关键词 game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev代码 / 文件 (136)
ling/modals.ling
# modals.ling - floating windows: text prompt, confirm, file picker, settings, scene settings,
# new project, about / help / shortcuts. One modal at a time (st "modal").
fn modal_open(name) {
st_set("modal", name)
st_set("modal_t0", st_get("now", 0.0))
st_set("focus", "")
}
fn modal_close() {
st_set("modal", "")
st_set("focus", "")
}
fn modal_text(title, kind, initial) {
st_set("text_title", title)
st_set("text_kind", kind)
modal_open("text")
st_set("text_val", initial)
text_set(initial)
st_set("focus", "text_modal")
}
fn confirm_open(title, message, act) {
st_set("confirm_title", title)
st_set("confirm_msg", message)
st_set("confirm_act", act)
modal_open("confirm")
}
fn info_open(title, message) {
st_set("info_title", title)
st_set("info_msg", message)
modal_open("info")
}
# window chrome; leaves the content origin in wx, wy
fn win_begin(title, w, h) {
bind W = get_width()
bind H = get_height()
bind w = min(w, W - 20.0)
bind h = min(h, H - 20.0)
bind x = floor((W - w) * 0.5)
bind y = floor((H - h) * 0.5) - 20.0
if y < 4.0 { bind y = 4.0 }
g_rect_a(0.0, 0.0, W, H, 0.0, 0.45)
px_rrect(x + 3.0, y + 5.0, w, h, 8.0, 0.0, 0.5)
g_rrect(x, y, w, h, 7.0, C_BG_PANEL)
g_rframe(x, y, w, h, 7.0, C_POPUP_LINE)
g_rrect(x, y, w, 26.0, 7.0, 2434344.0)
g_rect(x, y + 14.0, w, 12.0, 2434344.0)
g_text(x + 12.0, y + 6.0, 13.0, C_TEXT_STRONG, title)
bind hot = u_hot(x + w - 26.0, y + 3.0, 20.0, 20.0)
if hot { g_rrect(x + w - 26.0, y + 3.0, 20.0, 20.0, 4.0, C_RED) }
icon_draw("x", x + w - 16.0, y + 13.0, 10.0, C_TEXT_STRONG)
if hot && u_pressed() { modal_close() }
u_hsep(x, y + 26.0, w)
st_set("wx", x)
st_set("wy", y + 34.0)
st_set("ww", w)
st_set("wh", h - 34.0)
}
fn modal_overlay() {
bind m = st_get("modal", "")
if m == "" { return 0.0 }
st_set("blk", sel(st_get("cb_open", "") != "" || st_get("ctx_open", 0.0) > 0.5, 1.0, 0.0))
if m == "text" { modal_text_win() }
else if m == "confirm" { modal_confirm() }
else if m == "info" { modal_info() }
else if m == "picker" { modal_picker() }
else if m == "settings" { modal_settings() }
else if m == "scene" { modal_scene_settings() }
else if m == "newproject" { modal_new_project() }
else if m == "about" { modal_about() }
else if m == "help" { modal_help() }
else if m == "shortcuts" { modal_shortcuts() }
else if m == "scripting" { modal_scripting() }
else if m == "clip" { modal_clip() }
else if m == "addnode" { modal_addnode() }
if key_pressed("escape") && st_get("cb_open", "") == "" { modal_close() }
return 0.0
}
fn modal_confirm() {
win_begin(st_get("confirm_title", "Confirm"), 420.0, 140.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
g_text_wrap(x + 16.0, y + 10.0, 388.0, 13.0, C_TEXT, st_get("confirm_msg", ""))
if u_button("cf_yes", x + 16.0, y + 66.0, 110.0, 26.0, "Yes", C_RED) {
bind act = st_get("confirm_act", "")
modal_close()
do_action(act)
}
if u_button("cf_no", x + 134.0, y + 66.0, 110.0, 26.0, "Cancel", 0.0) { modal_close() }
if key_pressed("enter") {
bind act = st_get("confirm_act", "")
modal_close()
do_action(act)
}
}
fn modal_info() {
win_begin(st_get("info_title", "Info"), 520.0, 220.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind lines = str_split(st_get("info_msg", ""), "\n")
bind ly = y + 10.0
bind i = 0.0
while i < len(lines) {
bind ly = ly + g_text_wrap(x + 16.0, ly, 488.0, 12.0, C_TEXT, list_get(lines, i))
bind i = i + 1.0
}
if u_button("inf_ok", x + 520.0 * 0.5 - 50.0, y + 220.0 - 76.0, 100.0, 26.0, "OK", 0.0) { modal_close() }
if key_pressed("enter") { modal_close() }
}
fn modal_text_win() {
win_begin(st_get("text_title", "Rename"), 420.0, 120.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind v = u_textfield("text_modal", x + 14.0, y + 10.0, 392.0, 26.0, st_get("text_val", ""))
st_set("text_val", v)
bind okb = u_button("txt_ok", x + 14.0, y + 50.0, 80.0, 26.0, "OK", C_BLUE)
bind cancel = u_button("txt_cancel", x + 100.0, y + 50.0, 80.0, 26.0, "Cancel", 0.0)
if key_pressed("enter") { bind okb = true }
if okb {
bind kind = st_get("text_kind", "")
modal_close()
text_submit(kind, v)
return 0.0
}
if cancel { modal_close() }
return 0.0
}
# ── file picker (works on every runtime; native dialogs are a setting) ───────
# mode: open | save | folder. exts: "lscene,lcut" or "". act: action run with st pick_path set
fn picker_open(mode, title, exts, act, start_dir, name) {
st_set("pick_mode", mode)
st_set("pick_title", title)
st_set("pick_exts", exts)
st_set("pick_act", act)
st_set("pick_name", name)
st_set("pick_sel", "")
st_set("pick_scroll", 0.0)
bind d = start_dir
if d == "" || !fs_is_dir(d) { bind d = st_get("proj_root", "") }
if d == "" || !fs_is_dir(d) { bind d = fs_cwd() }
st_set("pick_dir", p_norm(d))
if plat_has_dialogs() && st_get("native_dialogs", 0.0) > 0.5 {
bind r = ""
if mode == "open" { bind r = plat_dlg_open(title, exts, d) }
else if mode == "save" { bind r = plat_dlg_save(title, exts, name, d) }
else { bind r = plat_dlg_folder(title, d) }
if r != "" {
st_set("pick_path", p_norm(r))
do_action(act)
}
return 0.0
}
modal_open("picker")
if mode == "save" {
st_set("focus", "pick_namef")
text_set(name)
}
return 0.0
}
fn pick_ext_ok(name) {
bind exts = st_get("pick_exts", "")
if exts == "" { return true }
bind e = p_ext(name)
bind l = str_split(exts, ",")
bind i = 0.0
while i < len(l) {
if str_trim(list_get(l, i)) == e { return true }
bind i = i + 1.0
}
return false
}
fn pick_go(dir) {
st_set("pick_dir", p_norm(dir))
st_set("pick_sel", "")
st_set("pick_scroll", 0.0)
}
fn pick_finish(path) {
bind act = st_get("pick_act", "")
modal_close()
st_set("pick_path", p_norm(path))
do_action(act)
}
fn modal_picker() {
bind ww = 760.0
bind wh = 540.0
win_begin(st_get("pick_title", "Open"), ww, wh)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind mode = st_get("pick_mode", "open")
bind dir = st_get("pick_dir", ".")
# places
bind px = x + 12.0
if u_button("pk_home", px, y + 6.0, 60.0, 24.0, "Home", 0.0) { pick_go(fs_home()) }
bind px = px + 64.0
if st_get("proj_root", "") != "" {
if u_button("pk_proj", px, y + 6.0, 70.0, 24.0, "Project", 0.0) { pick_go(st_get("proj_root", "")) }
bind px = px + 74.0
}
if u_button("pk_cwd", px, y + 6.0, 70.0, 24.0, "LingDev", 0.0) { pick_go(st_get("app_root", fs_cwd())) }
bind px = px + 74.0
if u_button("pk_up", px, y + 6.0, 40.0, 24.0, "Up", 0.0) {
bind parent = fs_parent(dir)
if parent != "" && parent != dir { pick_go(parent) }
}
bind px = px + 48.0
g_rrect(px, y + 6.0, x + ww - px - 12.0, 24.0, 3.0, C_BG_DEEP)
g_text_fit(px + 8.0, y + 12.0, x + ww - px - 28.0, 12.0, C_TEXT, dir)
# listing
bind ly = y + 40.0
bind lh = wh - 40.0 - 100.0
g_rect(x + 12.0, ly, ww - 24.0, lh, C_BG_DEEP)
g_frame(x + 12.0, ly, ww - 24.0, lh, C_LINE)
bind items = fs_list(dir)
bind n = len(items) / 3.0
bind rows = []
bind i = 0.0
while i < n {
bind nm = list_get(items, i * 3.0)
bind isd = is_true(list_get(items, i * 3.0 + 1.0))
if isd || mode != "folder" {
if isd || pick_ext_ok(nm) { bind rows = list_push(rows, i) }
}
bind i = i + 1.0
}
bind rh = 22.0
bind total = len(rows) * rh + 8.0
bind off = u_scroll_y("pk_sb", x + 12.0, ly, ww - 24.0, lh, total, st_get("pick_scroll", 0.0))
st_set("pick_scroll", off)
u_clip(x + 13.0, ly + 1.0, ww - 26.0, lh - 2.0)
bind r = 0.0
while r < len(rows) {
bind i = list_get(rows, r)
bind nm = list_get(items, i * 3.0)
bind isd = is_true(list_get(items, i * 3.0 + 1.0))
bind size = list_get(items, i * 3.0 + 2.0)
bind ry = ly + 4.0 + r * rh - off
if ry + rh >= ly && ry <= ly + lh {
bind full = p_join(dir, nm)
bind selected = st_get("pick_sel", "") == full
bind hot = u_hot(x + 14.0, ry, ww - 40.0, rh)
if selected { g_rect(x + 14.0, ry, ww - 40.0, rh, C_SEL_ROW) }
else if hot { g_rect(x + 14.0, ry, ww - 40.0, rh, C_HOVER_ROW) }
bind e = p_ext(nm)
if isd { icon_draw("folder", x + 28.0, ry + rh * 0.5, 13.0, C_FOLDER) }
else { icon_draw(ext_icon(e), x + 28.0, ry + rh * 0.5, 13.0, ext_col(e)) }
g_text_fit(x + 44.0, ry + 5.0, ww - 170.0, 12.0, sel(selected, C_TEXT_STRONG, C_TEXT), nm)
if !isd { g_text_r(x + ww - 30.0, ry + 6.0, 11.0, C_TEXT_DIM, sel(size > 1048576.0, fmt1(size / 1048576.0) + " MB", sel(size > 1024.0, num_str(floor(size / 1024.0)) + " KB", num_str(size) + " B"))) }
if hot && u_pressed() {
st_set("pick_sel", full)
if !isd && mode == "save" {
st_set("pick_name", nm)
text_set(nm)
}
if u_double() {
if isd { pick_go(full) }
else if mode == "open" { pick_finish(full) }
}
}
}
bind r = r + 1.0
}
if len(rows) < 0.5 { g_text(x + 24.0, ly + 10.0, 12.0, C_TEXT_DIM, "(empty)") }
u_unclip()
# bottom row
bind by = y + wh - 92.0
if mode == "save" {
g_text(x + 12.0, by + 6.0, 12.0, C_TEXT_DIM, "Name")
bind nv = u_textfield("pick_namef", x + 56.0, by, ww - 68.0, 26.0, st_get("pick_name", ""))
st_set("pick_name", nv)
} else {
g_text_fit(x + 12.0, by + 6.0, ww - 24.0, 12.0, C_TEXT_DIM, sel(st_get("pick_sel", "") == "", "Filter: " + sel(st_get("pick_exts", "") == "", "all files", st_get("pick_exts", "")), st_get("pick_sel", "")))
}
bind by = by + 36.0
bind oklabel = sel(mode == "save", "Save", sel(mode == "folder", "Choose Folder", "Open"))
bind ok = u_button("pk_ok", x + ww - 232.0, by, 110.0, 28.0, oklabel, C_BLUE)
if key_pressed("enter") && st_get("focus", "") != "" { bind ok = true }
if ok {
if mode == "save" {
bind nm = str_trim(st_get("pick_name", ""))
if nm != "" {
bind exts = st_get("pick_exts", "")
if exts != "" && !pick_ext_ok(nm) { bind nm = nm + "." + str_trim(list_get(str_split(exts, ","), 0)) }
pick_finish(p_join(dir, nm))
}
} else if mode == "folder" {
bind s = st_get("pick_sel", "")
pick_finish(sel(s != "" && fs_is_dir(s), s, dir))
} else {
bind s = st_get("pick_sel", "")
if s != "" {
if fs_is_dir(s) { pick_go(s) } else { pick_finish(s) }
}
}
}
if u_button("pk_cancel", x + ww - 116.0, by, 104.0, 28.0, "Cancel", 0.0) { modal_close() }
}
# ── settings ─────────────────────────────────────────────────────────────────
fn modal_settings() {
win_begin("Settings", 520.0, 360.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
ds_word(x + 16.0, y + 4.0, 11.0, "EDITOR", ds_col("call"), true)
bind yy = y + 32.0
bind v = ds_switch("set_native", x + 16.0, yy, 16.0, "Native file dialogs", st_get("native_dialogs", 0.0) > 0.5, ds_col("ctl"), "Use the system Open / Save dialogs instead of the built-in picker")
if v >= 0.0 { st_set("native_dialogs", v) }
bind yy = yy + 28.0
bind v = ds_switch("set_black", x + 16.0, yy, 16.0, "Black theme", st_get("theme_black", 0.0) > 0.5, ds_col("mod"), "Near-black chrome (LingStation's Black scheme)")
if v >= 0.0 { st_set("theme_black", v) }
bind yy = yy + 28.0
bind v = ds_switch("set_loadlast", x + 16.0, yy, 16.0, "Open the last project at start", st_get("load_last", 1.0) > 0.5, ds_col("bind"), "")
if v >= 0.0 { st_set("load_last", v) }
bind yy = yy + 28.0
bind v = ds_switch("set_sounds", x + 16.0, yy, 16.0, "Interface sounds", st_get("ui_sounds", 0.0) > 0.5, ds_col("asm"), "")
if v >= 0.0 { st_set("ui_sounds", v) }
bind yy = yy + 36.0
ds_word(x + 16.0, yy, 11.0, "VIEWPORT", ds_col("func"), true)
bind yy = yy + 28.0
g_text(x + 16.0, yy + 5.0, 12.0, C_TEXT, "Grid step")
bind nv = u_dragval("set_grid", x + 120.0, yy, 80.0, 24.0, st_get("grid_step", 1.0), 0.1, 10.0, 0.02, fmt2(st_get("grid_step", 1.0)))
if u_has(nv) { st_set("grid_step", round(nv * 100.0) / 100.0) }
g_text(x + 220.0, yy + 5.0, 12.0, C_TEXT, "Orbit speed")
bind nv = u_dragval("set_orbit", x + 310.0, yy, 80.0, 24.0, st_get("orbit_speed", 1.0), 0.2, 3.0, 0.01, fmt2(st_get("orbit_speed", 1.0)))
if u_has(nv) { st_set("orbit_speed", round(nv * 100.0) / 100.0) }
bind yy = yy + 32.0
g_text(x + 16.0, yy + 5.0, 12.0, C_TEXT, "Autosave (min)")
bind nv = u_dragval("set_autosave", x + 120.0, yy, 80.0, 24.0, st_get("autosave", 3.0), 0.0, 60.0, 0.1, num_str(floor(st_get("autosave", 3.0))))
if u_has(nv) { st_set("autosave", floor(nv)) }
g_text(x + 220.0, yy + 5.0, 12.0, C_TEXT, "ling.exe")
bind lp = u_textfield("set_ling", x + 290.0, yy, 210.0, 24.0, st_get("ling_exe", ""))
st_set("ling_exe", lp)
bind yy = yy + 44.0
if u_button("set_ok", x + 16.0, yy, 100.0, 26.0, "Close", C_BLUE) {
settings_save()
modal_close()
}
g_text(x + 130.0, yy + 7.0, 11.0, C_TEXT_DIM, "Settings are kept in settings.txt next to LingDev.")
}
# ── scene settings ───────────────────────────────────────────────────────────
fn modal_scene_settings() {
win_begin("Scene Settings", 540.0, 434.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind yy = y + 6.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Name")
bind nv = u_textfield("scn_name", x + 110.0, yy, 300.0, 26.0, sc_get("name", ""))
if nv != sc_get("name", "") { sc_set("name", nv) st_set("dirty", 1.0) }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Kind")
bind kinds = ["3d", "2d", "cutscene"]
bind kn = ["3D scene / level", "2D menu / HUD", "Cutscene"]
bind ki = u_combo("scn_kind", x + 110.0, yy, 200.0, 26.0, kn, max(0.0, l_index(kinds, sc_get("kind", "3d"))))
if ki >= 0.0 { sc_set("kind", list_get(kinds, ki)) st_set("dirty", 1.0) }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Background")
bind bg = sc_get("bg", 0.0)
bind r = u_dragval("scn_bgr", x + 110.0, yy, 54.0, 26.0, c_r(bg), 0.0, 255.0, 1.0, num_str(c_r(bg)))
bind g = u_dragval("scn_bgg", x + 168.0, yy, 54.0, 26.0, c_g(bg), 0.0, 255.0, 1.0, num_str(c_g(bg)))
bind b = u_dragval("scn_bgb", x + 226.0, yy, 54.0, 26.0, c_b(bg), 0.0, 255.0, 1.0, num_str(c_b(bg)))
if u_has(r) { sc_set("bg", c_rgb(r, c_g(bg), c_b(bg))) st_set("dirty", 1.0) }
if u_has(g) { sc_set("bg", c_rgb(c_r(bg), g, c_b(bg))) st_set("dirty", 1.0) }
if u_has(b) { sc_set("bg", c_rgb(c_r(bg), c_g(bg), b)) st_set("dirty", 1.0) }
g_rect(x + 290.0, yy + 2.0, 40.0, 22.0, sc_get("bg", 0.0))
g_frame(x + 290.0, yy + 2.0, 40.0, 22.0, C_LINE)
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Timeline")
bind nv = u_dragval("scn_len", x + 110.0, yy, 80.0, 26.0, sc_get("len", 10.0), 0.5, 3600.0, 0.05, fmt1(sc_get("len", 10.0)) + " s")
if u_has(nv) { sc_set("len", round(nv * 10.0) / 10.0) st_set("dirty", 1.0) }
bind nv = u_dragval("scn_fps", x + 196.0, yy, 80.0, 26.0, sc_get("fps", 30.0), 1.0, 120.0, 0.2, num_str(floor(sc_get("fps", 30.0))) + " fps")
if u_has(nv) { sc_set("fps", floor(nv)) st_set("dirty", 1.0) }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Ambient")
bind nv = u_hslider("scn_amb", x + 110.0, yy + 2.0, 200.0, 22.0, sc_get("amb", 0.25), 0.0, 1.0, C_YELLOW)
if u_has(nv) { sc_set("amb", nv) st_set("dirty", 1.0) }
g_text(x + 320.0, yy + 6.0, 12.0, C_TEXT_DIM, fmt2(sc_get("amb", 0.25)))
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "HUD scene")
bind hv = u_textfield("scn_hud", x + 110.0, yy, 300.0, 26.0, sc_get("hud", ""))
if hv != sc_get("hud", "") { sc_set("hud", hv) st_set("dirty", 1.0) }
if u_button("scn_hud_pick", x + 416.0, yy, 60.0, 26.0, "Pick", 0.0) { picker_open("open", "HUD scene (2D) drawn over this scene", "lscene", "scene.set_hud", st_get("proj_root", ""), "") }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Next scene")
bind nxv = u_textfield("scn_next", x + 110.0, yy, 300.0, 26.0, sc_get("next", ""))
if nxv != sc_get("next", "") { sc_set("next", nxv) st_set("dirty", 1.0) }
if u_button("scn_next_pick", x + 416.0, yy, 60.0, 26.0, "Pick", 0.0) { picker_open("open", "Scene to load when this one ends", "lscene", "scene.set_next", st_get("proj_root", ""), "") }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Music")
bind mv = u_textfield("scn_music", x + 110.0, yy, 300.0, 26.0, sc_get("music", ""))
if mv != sc_get("music", "") { sc_set("music", mv) st_set("dirty", 1.0) }
if u_button("scn_music_pick", x + 416.0, yy, 60.0, 26.0, "Pick", 0.0) { picker_open("open", "Background music", "wav,ogg,mp3,flac", "scene.set_music", st_get("proj_root", ""), "") }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Fog r,g,b,near,far")
bind fogv = u_textfield("scn_fog", x + 150.0, yy, 260.0, 26.0, sc_ex("fog", ""))
if fogv != sc_ex("fog", "") { sc_ex_set("fog", fogv) st_set("dirty", 1.0) }
g_text(x + 416.0, yy + 6.0, 11.0, C_TEXT_DIM, "e.g. 30,72,158,8,34")
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Gravity / floor")
bind nv = u_dragval("scn_grav", x + 110.0, yy, 80.0, 26.0, sc_get("grav", 9.0), 0.0, 60.0, 0.05, fmt1(sc_get("grav", 9.0)))
if u_has(nv) { sc_set("grav", round(nv * 10.0) / 10.0) st_set("dirty", 1.0) }
bind nv = u_dragval("scn_floor", x + 196.0, yy, 80.0, 26.0, sc_get("floor", 0.0), 0.0 - 1000.0, 1000.0, 0.05, fmt1(sc_get("floor", 0.0)))
if u_has(nv) { sc_set("floor", round(nv * 10.0) / 10.0) st_set("dirty", 1.0) }
bind yy = yy + 40.0
if u_button("scn_ok", x + 16.0, yy, 100.0, 26.0, "Close", C_BLUE) { modal_close() }
}
# ── new project ──────────────────────────────────────────────────────────────
fn modal_new_project() {
win_begin("New Project", 560.0, 330.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind yy = y + 8.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Name")
bind nv = u_textfield("np_name", x + 110.0, yy, 300.0, 26.0, st_get("np_name", "MyGame"))
st_set("np_name", nv)
if st_get("focus", "") == "" && st_get("np_first", 0.0) < 0.5 { st_set("focus", "np_name") text_set(nv) st_set("np_first", 1.0) }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Folder")
g_rrect(x + 110.0, yy, 300.0, 26.0, 3.0, C_BG_DEEP)
g_text_fit(x + 118.0, yy + 6.0, 284.0, 12.0, C_TEXT, st_get("np_dir", p_join(st_get("app_root", "."), "projects")))
if u_button("np_pick", x + 416.0, yy, 60.0, 26.0, "Pick", 0.0) { picker_open("folder", "Folder that will hold the project folder", "", "project.new_dir", st_get("np_dir", ""), "") }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Template")
bind tn = ["Empty 3D scene", "3D level + 2D HUD", "2D menu + 3D background", "Cutscene demo", "Rhythm / MIDI demo"]
bind ti = u_combo("np_tpl", x + 110.0, yy, 300.0, 26.0, tn, st_get("np_tpl", 1.0))
if ti >= 0.0 { st_set("np_tpl", ti) }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Window")
bind nv = u_dragval("np_w", x + 110.0, yy, 80.0, 26.0, st_get("np_w", 1280.0), 320.0, 3840.0, 2.0, num_str(st_get("np_w", 1280.0)))
if u_has(nv) { st_set("np_w", floor(nv)) }
bind nv = u_dragval("np_h", x + 196.0, yy, 80.0, 26.0, st_get("np_h", 720.0), 200.0, 2160.0, 2.0, num_str(st_get("np_h", 720.0)))
if u_has(nv) { st_set("np_h", floor(nv)) }
bind yy = yy + 40.0
g_text_wrap(x + 16.0, yy, 520.0, 11.0, C_TEXT_DIM, "Creates <folder>/<name>/ with lingdev.toml, Ling.toml, scenes/, scripts/, assets/ and the runtime (rt/) so `ling run main.ling` and `ling build .` work from day one.")
bind yy = yy + 40.0
if u_button("np_ok", x + 16.0, yy, 120.0, 28.0, "Create", C_BLUE) {
modal_close()
do_action("project.create")
}
if u_button("np_cancel", x + 144.0, yy, 100.0, 28.0, "Cancel", 0.0) { modal_close() }
}
# ── add node palette ─────────────────────────────────────────────────────────
fn modal_addnode() {
win_begin("Add to Scene", 600.0, 420.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind k2d = sc_get("kind", "3d") == "2d"
bind kinds = sel(k2d, nd_kinds_2d(), nd_kinds_3d())
bind cols = 4.0
bind cw = 138.0
bind ch = 64.0
bind i = 0.0
while i < len(kinds) {
bind k = list_get(kinds, i)
bind cx = x + 16.0 + (i % cols) * (cw + 8.0)
bind cy = y + 8.0 + floor(i / cols) * (ch + 8.0)
bind col = kind_col(k)
bind hot = u_hot(cx, cy, cw, ch)
px_rect(cx, cy, cw, ch, c_mix(C_BG_PANEL, col, sel(hot, 0.12, 0.03)))
if hot { ds_cross(cx + 1.0, cy + 1.0, cw - 2.0, ch - 2.0, col, 4.0, 0.16) }
px_outline(cx, cy, cw, ch, sel(hot, col, c_mix(C_BG_PANEL, col, 0.55)), 0.8)
ds_brackets(cx, cy, cw, ch, sel(hot, 12.0, 6.0), col)
icon_draw(kind_icon(k), cx + 26.0, cy + ch * 0.5, 26.0, col)
g_text(cx + 50.0, cy + 18.0, 13.0, C_TEXT_STRONG, k)
g_text_fit(cx + 50.0, cy + 36.0, cw - 56.0, 10.0, C_TEXT_DIM, kind_blurb(k))
if hot && u_pressed() {
modal_close()
do_action("node.add:" + k)
}
bind i = i + 1.0
}
if u_button("an_cancel", x + 600.0 * 0.5 - 50.0, y + 420.0 - 74.0, 100.0, 26.0, "Cancel", 0.0) { modal_close() }
}
fn kind_blurb(k) {
if k == "box" { return "Solid block, wall, floor" }
if k == "sphere" { return "Ball, planet, orb" }
if k == "cylinder" { return "Pillar, tube" }
if k == "cone" { return "Spike, roof" }
if k == "capsule" { return "Character proxy" }
if k == "torus" { return "Ring, donut" }
if k == "plane" { return "Ground plane" }
if k == "model" { return "glb / gltf / lmesh file" }
if k == "actor" { return "Rigged model + controller" }
if k == "light" { return "Point light" }
if k == "camera" { return "Fixed, orbit, follow, dolly" }
if k == "spawn" { return "Where the player starts" }
if k == "trigger" { return "Zone that fires an event" }
if k == "audio" { return "Positional sound" }
if k == "music" { return "Background track" }
if k == "midi" { return "MIDI notes track" }
if k == "text3d" { return "Text in the world" }
if k == "empty" { return "Group / pivot" }
if k == "swarm" { return "Stars, wire cubes, orbs" }
if k == "knot" { return "Rosette chain of lit cubes" }
if k == "panel" { return "Backdrop rectangle" }
if k == "label" { return "Text" }
if k == "button" { return "Clickable, runs an action" }
if k == "image" { return "Picture (png / jpg)" }
if k == "bar" { return "Health / progress" }
return ""
}
# ── about / help / shortcuts / scripting ─────────────────────────────────────
fn modal_about() {
win_begin("About LingDev", 520.0, 300.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
icon_draw("cube", x + 48.0, y + 44.0, 48.0, C_BLUE)
ds_word(x + 96.0, y + 14.0, 16.0, "LINGDEV", ds_col("call"), true)
g_text(x + 96.0, y + 40.0, 12.0, C_TEXT, "A scene, timeline and cutscene editor for games written in Ling.")
g_text(x + 96.0, y + 58.0, 12.0, C_TEXT_DIM, "Runs on the desktop ling runtime and as a LingOS app (Raspberry Pi).")
g_text(x + 16.0, y + 100.0, 12.0, C_TEXT, "Engine: ling " + st_get("ling_version", "2030.1") + " runtime: " + plat_name())
g_text(x + 16.0, y + 120.0, 12.0, C_TEXT, "Project: " + sel(st_get("proj_root", "") == "", "(none)", st_get("proj_root", "")))
g_text_wrap(x + 16.0, y + 150.0, 488.0, 11.0, C_TEXT_DIM, "Scenes are .lscene text files; games export with `ling build` for Windows, Linux, macOS, the web, a Raspberry Pi kernel image or a LingOS app. Written in Ling, in the look of LingStationX.")
if u_button("ab_ok", x + 16.0, y + 220.0, 100.0, 26.0, "Close", C_BLUE) { modal_close() }
}
fn modal_help() {
win_begin("Help", 640.0, 480.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind lines = help_lines()
bind off = u_scroll_y("help_sb", x + 8.0, y + 4.0, 624.0, 400.0, len(lines) * 18.0 + 20.0, st_get("help_scroll", 0.0))
st_set("help_scroll", off)
u_clip(x + 8.0, y + 4.0, 616.0, 400.0)
bind i = 0.0
while i < len(lines) {
bind ln = list_get(lines, i)
bind ly = y + 8.0 + i * 18.0 - off
if str_starts_with(ln, "#") { ds_word(x + 16.0, ly, 10.0, str_slice(ln, 1.0, str_len(ln) - 1.0), ds_col("call"), true) }
else { g_text_fit(x + 16.0, ly, 596.0, 12.0, C_TEXT, ln) }
bind i = i + 1.0
}
u_unclip()
if u_button("hp_ok", x + 16.0, y + 416.0, 100.0, 26.0, "Close", C_BLUE) { modal_close() }
}
fn modal_shortcuts() {
win_begin("Shortcuts", 640.0, 480.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind lines = shortcut_lines()
bind off = u_scroll_y("sc_sb", x + 8.0, y + 4.0, 624.0, 400.0, len(lines) * 18.0 + 20.0, st_get("sc_scroll", 0.0))
st_set("sc_scroll", off)
u_clip(x + 8.0, y + 4.0, 616.0, 400.0)
bind i = 0.0
while i < len(lines) {
bind ln = list_get(lines, i)
bind ly = y + 8.0 + i * 18.0 - off
if str_starts_with(ln, "#") { ds_word(x + 16.0, ly, 10.0, str_slice(ln, 1.0, str_len(ln) - 1.0), ds_col("bind"), true) }
else {
bind p = str_split(ln, "|")
g_text(x + 16.0, ly, 12.0, C_YELLOW, list_get(p, 0))
if len(p) > 1.0 { g_text(x + 190.0, ly, 12.0, C_TEXT, list_get(p, 1)) }
}
bind i = i + 1.0
}
u_unclip()
if u_button("sh_ok", x + 16.0, y + 416.0, 100.0, 26.0, "Close", C_BLUE) { modal_close() }
}
fn modal_scripting() {
win_begin("Scripting Reference", 700.0, 520.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind lines = scripting_lines()
bind off = u_scroll_y("scr_sb", x + 8.0, y + 4.0, 684.0, 440.0, len(lines) * 17.0 + 20.0, st_get("scr_scroll", 0.0))
st_set("scr_scroll", off)
u_clip(x + 8.0, y + 4.0, 676.0, 440.0)
bind i = 0.0
while i < len(lines) {
bind ln = list_get(lines, i)
bind ly = y + 8.0 + i * 17.0 - off
if str_starts_with(ln, "#") { ds_word(x + 16.0, ly, 10.0, str_slice(ln, 1.0, str_len(ln) - 1.0), ds_col("ctl"), true) }
else if str_starts_with(ln, " ") { g_text_fit(x + 16.0, ly, 656.0, 11.0, C_GREEN, ln) }
else { g_text_fit(x + 16.0, ly, 656.0, 12.0, C_TEXT, ln) }
bind i = i + 1.0
}
u_unclip()
if u_button("scr_ok", x + 16.0, y + 456.0, 100.0, 26.0, "Close", C_BLUE) { modal_close() }
}