diff --git a/.forgejo/workflows/bootstrap.yml b/.forgejo/workflows/bootstrap.yml index 926e4922..a41bb6e5 100644 --- a/.forgejo/workflows/bootstrap.yml +++ b/.forgejo/workflows/bootstrap.yml @@ -35,7 +35,7 @@ jobs: git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}" git log --oneline -1 # See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC. - echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV" + echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV" echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV" - name: Bootstrap x from the seed diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index afd5c567..04dd75a3 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: # LUDIC_CC so every clang invocation — the seed bootstrap, `ludic-dev build`, # and each compiled test program — picks it up. Absolute path so it # still resolves if a step changes directory. - echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV" + echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV" echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV" - name: Bootstrap the toolchain from the IR seed (clang only) diff --git a/.forgejo/workflows/docs.yml b/.forgejo/workflows/docs.yml index fd8f5f20..b606bf3e 100644 --- a/.forgejo/workflows/docs.yml +++ b/.forgejo/workflows/docs.yml @@ -63,7 +63,7 @@ jobs: # tiny C-free IR shim supplying the Darwin stdout/stderr globals over # glibc's, injected through LUDIC_CC. docs-gen is a pure CLI (no # windowing), so the C-free bootstrap is all it needs. - export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" + export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" export LUDIC_HOME="$(pwd)" mkdir -p bin clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 45e928e8..1e323525 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -49,7 +49,7 @@ jobs: git checkout "$TAG" echo "TAG=$TAG" >> "$GITHUB_ENV" # See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC. - echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV" + echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV" echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV" - name: The tag, VERSION and CHANGELOG must agree diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..759f9796 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +packages/*/lib/** filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 1a82f70e..ec21dc03 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ tools/editors/vscode/node_modules/ tools/editors/vscode/*.vsix tools/editors/jetbrains/.gradle/ tools/editors/jetbrains/build/ +tools/editors/jetbrains/.kotlin/ # IntelliJ plugin SDK sandbox (tools/editors/jetbrains) .intellijPlatform/ @@ -54,3 +55,8 @@ __pycache__/ assets/polyhaven/hdri/ assets/polyhaven/textures/ assets/polyhaven/models/ +# `ludic run` beside an example writes its binary into a build/ there +examples/**/build/ + +# a package native/build.sh writes its objects under the package (phase 15) +packages/*/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d9494163..33ed5947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,348 @@ change type. Preview the next one with `ludic dev release --dry-run`. A released section may carry a hand-written summary paragraph above its groups (v0.2.0 has one); the generated bullets below it are not edited by hand. +## v0.22.0 — 2026-09-18 + +### Features + +- **Every key on the keyboard is bindable** — the F-row, Home/End/PageUp/PageDown, + Insert/Delete, Caps Lock and the numpad reach a game as codes 132-166. + + Until now the platform gave them no code at all, so a game's rebinding screen could + not take one and nothing said why. Windows asked the active layout what they type and + got nothing back (`w_vk_char` answers 0 for a key with no character); macOS let them + fall through to `charactersIgnoringModifiers`, which reports `NSF1FunctionKey` and its + neighbours at 0xF704 and up - outside the 256-bit held set either way. + + - **The codes are the same on both platforms**: 132-143 are F1-F12, 144-149 are Home, + End, PageUp, PageDown, Insert and Delete, 150 is Caps Lock, 152-161 are the numpad + digits and 162-166 its `*`, `+`, `-`, `.` and `/`. The numpad's Enter is Enter. + - **`Key.F1`, `Key.Home`, `Key.Numpad0`** and the rest fold at compile time like the + other named keys. + - **`Input.key_label`** names them - "F1", "Home", "CapsLock", "Num 0", "Num /" - and + does *not* ask the layout, because a key that types nothing is called the same thing + on every layout. +## v0.21.1 — 2026-09-17 + +### Fixes + +- A windowed game opens under its package's `app name`. `ludic build`, `ludic run` and + `ludic bundle` pass it to the compiler (`ludicc --title `), so `game_title()` - the + window's first title - is the name the player knows rather than the `program` name, which + showed for up to two seconds before the renderer retitled the window. Without an `app name` + it is the `program` name, as before. +- `Crypto.random_bytes`, `Crypto.random_hex`, `Crypto.random_u32` and `Uuid.*` draw from the + system CSPRNG on Windows (`RtlGenRandom`, advapi32). They read `/dev/urandom`, which Windows + does not have, and every byte came back zero. +## v0.21.0 — 2026-09-17 + +### Features + +- **A launcher's runtime** — start a program, step aside while it runs, and download large + files with a progress bar. + + - **`Process.*`** — `Process.spawn(path, args)` starts a program directly (no shell) with this + process's environment and working directory and returns at once; `Process.poll(h)` is `-1` + while it runs, then its exit code (`128 + signal` for a signal death, `137` after + `Process.kill`); `Process.free` lets a handle go. `posix_spawn` on macOS + (`runtime/native/process.ll`); `CreateProcessW` on Windows (`process_win.ll`), with each + argument quoted by the MSVC rules and no console window. Linked only into a program that + uses `Process.*`. + - **`Http.save_to(h, path)`** — before `Http.send`, streams the response body straight into a + file (written at `path` itself, created on send) instead of memory; `Http.text` of such a + request is empty and `Http.body_len` is the bytes written. **`Http.received(h)`** and + **`Http.expected(h)`** (the `Content-Length`, or `-1`) are live while it is pending. macOS + streams through an `NSURLSession` with a delegate built at run time, Windows through the + WinHTTP read loop. Freeing a pending request now cancels it and parks its slot until the + worker has let go, rather than freeing what the worker is still writing. + - **`App.window_hide()` / `App.window_show()`** — take the game's window off the screen and + bring it back without closing it or its GL / Vulkan surface; the run goes on while it is + hidden. No-ops headless and before there is a window. + +### Fixes + +- A game's window takes the title it asks for. The runtime opens a game's window before `main`, + titled with the `program` name, and `Gl.open` / render3d's `gl_open` and `gvk_open` attached + to it without passing their title on, so a `program Valley` that opened "Maroon Lake" showed + "Valley". `win_set_title` (cocoa.ll `setTitle:`, win32.ll `SetWindowTextW`, UTF-8 to UTF-16) + now retitles it, and a second `win_open` on macOS retitles the open window instead of making + another, as it already did on Windows. +## v0.20.0 — 2026-09-17 + +### Features + +- The JetBrains plugin (1.6.0) has a Ludic tool window for the package, like the Gradle one: + - an overview of the package (what it is, entry, version, dependencies, scripts, hooks, app); + - its commands, scripts and hooks, run by double-click, with each command's hooks shown; + - its dependencies against package.lock.ludic: the locked version, whether it is fetched + and linked, the indirect ones, and Fetch, Update, Verify, Vendor, Add and Remove; + - the app preview the `app` lines describe, and the asset roots with missing ones marked. + The bar over package.ludic now appears only when something needs doing (fetch, or no + toolchain), instead of carrying every command. +## v0.19.0 — 2026-09-17 + +### Features + +- `Udp.*`: polled IPv4 datagrams - the transport under a game's own netcode. `Udp.open(port)` + binds a non-blocking socket (port 0 picks one, `Udp.port` says which), `Udp.send` sends one + datagram to an address and port, and `Udp.recv` drains what arrived without ever blocking; + `Udp.from_ip` / `Udp.from_port` name the sender. `Udp.ip` / `Udp.ip_text` convert addresses, + `Udp.resolve` looks a host name up and `Udp.local_ip` is the address a player on the same + network would dial. BSD sockets on macOS (`runtime/native/udp.ll`), Winsock on Windows + (`udp_win.ll`, linked with ws2_32), linked only into a program that uses `Udp.*`. +## v0.18.1 — 2026-09-16 + +### Fixes + +- The release build links libm on Linux, which `float`/`double` code needs (glibc keeps + `sinf`, `sqrtf` and the rest there). 0.18.0's release job stopped on this before + publishing, so 0.18.1 is the first published build with float types, barrel imports, + package scripts and hooks, and the JetBrains 1.5.0 support. +## v0.18.0 — 2026-09-16 + +### Features + +- Run a single test by name: a compiled test program takes the test's exact name as its + first argument (`./tests "adds"`), and `ludic test` gains `--test NAME` to pass it through. + A name that matches no test is reported and fails the run. `ludic test --verbose` (`-v`) + prints every test's `ok`/`FAIL` line, framed by `RUN ` … `PASS|FAIL `, which is + what editor test views parse. +- The JetBrains plugin covers much more of the IDE: + - Run configurations for `ludic run`/`build`/`test` (plus any other `ludic` command), with + gutter run buttons on `program` and on every `test "name"` block. + - Test results appear in the IDE's test tree, with navigation back to each test block. + - `file.ludic:line` locations are clickable in every console. + - Smart indentation on Enter and when typing a closing bracket; quotes pair up. + - A Color Scheme page and a Code Style page. + - Live templates, New Ludic File templates, and a Ludic project in File | New | Project. + - A Tools | Ludic menu, and a warning when the toolchain is missing. + - `package.ludic` is treated as a package manifest: + - a banner offers Fetch, Update, Verify, Run, Test, Build and Bundle, and warns when the lock is + missing or older than the manifest; + - gutter actions on `package` and `require` lines; + - completion and quick documentation for directives and `app` keys; + - its own icon. + - `script` and `hook` lines complete and are validated; each script has a run button, the + package banner has script shortcuts, and Tools | Ludic | Run Script… lists them all. + - The toolchain's and the project's packages are listed under External Libraries. + - The Structure view, parameter info and code-block navigation now come from `ludic-lsp`. + - The plugin is now 1.5.0 and requires IntelliJ Platform 2024.2 or newer and LSP4IJ 0.21. + - Fixed: Enter no longer inserts an extra `}`, and Comment Line no longer writes two spaces. +- `float` and `double`: IEEE floating point with ordinary operators, so renderer math reads + `a * b + c` instead of `f_add(f_mul(a, b), c)`. + - Decimal literals take their type from context (`let s: float = 0.1` is exactly 0.1), and + stay `fixed` elsewhere, so existing programs keep their meaning. + - `int`/`long` promote; `float(x)`, `double(x)`, `int(x)`, `long(x)` and `fixed(x)` convert. + - `floats(n)` / `doubles(n)` buffers; float fields, globals, constants and parameters. + - `Math.*` computes in float when given one; `string`/`print`/interpolation write the + shortest round-tripping decimal; `float_bits` / `float_from_bits` expose the IEEE pattern. + - `@deterministic` code may not use floats (it is now checked). +- `import "camp"` imports a directory through its barrel, `camp/index.ludic` (a fragment + listing the directory's own imports). A directory without one is a clear error. Packages + resolve the same way: `import "ludic.render3d"` reads `ludic.render3d/index.ludic`. +- `ludic-lsp` navigates like the compiler resolves: + - Package imports (`import "ludic.render3d/r3d.ludic"`) resolve from `ludic_modules/` and the + toolchain's `packages/`, so their names hover, complete and jump. Import strings are links. + - `var` locals are tracked (declaration, usages, rename). + - Hover shows the declared or inferred type of locals, parameters and globals. + - Hover and go-to-definition on built-ins, built-in types and namespaces show the reference + page, with a link to the online docs. Release archives now ship `docs/language` for this. + - Go to type declaration (`textDocument/typeDefinition`) is supported. + - Signature help carries per-parameter ranges and follows named arguments. + - Completion inside a call offers the parameter names not given yet, `Namespace.` offers its + members, and results are ranked: locals first, then globals, built-ins and keywords. +- `package.ludic` gains `entry`, scripts and lifecycle hooks: + - `entry "src/game.ludic"` names the program `ludic run`/`build`/`bundle` compile. Without + it, the one file under `src/` that declares a `program` is used. + - `script "dev" "ludic run --headless"` defines a command: `ludic dev` (or `ludic script dev`) + runs it with any extra arguments, and `ludic scripts` lists them. + - `hook before|after "…"` wraps `build`, `run`, `test`, `bundle`, `pack`, `get`, … + or a script. A failing `before` hook stops the command; `after` hooks run only on success. + - Quoted manifest values accept `\"` and `\\` escapes. + +### Fixes + +- **`==` / `!=` on records is identity again** — only two strings compare by content. + + Every pointer-typed comparison used to be lowered to a C-string content compare, + so two distinct records (properties, slices, enums) compared their bytes up to + the first zero byte: `a == b` could be true for different objects that shared a + leading field, depending on layout. References now compare by identity + (`icmp eq ptr`); `string` (and untyped `pointer`/`pointers` text) still compares by content, + and comparing a `string` with a non-string reference is a compile error. +## v0.17.0 — 2026-09-16 + +### Features + +- **Renderer switches stay out of shipped games** — every `R3D_*` environment switch in + `ludic.render3d` (debug views, feature kills, file writers, hardware fakes, the Windows + feature overrides) now goes through one gate: a headless build honours them as before, a + windowed build only when `R3D_DEV` is set to anything but `0`. See `docs/SHIPPING.md`. + +### Fixes + +- **A lake can be the water that mirrors the world** — a reflecting body is clipped to its + ellipse like any other unless it is an unbounded sea, so a map whose reflection belongs to + its lake no longer floods every hollow in the survey with that lake's level; and the + terrain's wet shore, forest and scree gates read the carved lake's line inside its outline + (`u_lake`), the rule the grass already used, so a lake above the sea keeps its wet bank and + no forest is painted down its bed. + + `terrain_lake_carve(false)` keeps a lake's line, outline and shore but skips carving its + bed, for a height map that already carries a shaped one. +## v0.16.2 — 2026-09-15 + +### Fixes + +- **Keys are physical positions on Windows and macOS, whatever the layout** - `Input.key_down('w')` + is the key above S on every keyboard. + + - **Windows** keyed the held set by the character the active layout gave a virtual key, so + a game's WASD belonged to whatever the layout put there: on AZERTY W and A were other + keys, and with an input method on (Chinese, Japanese, Korean) every letter arrived as + VK_PROCESSKEY and no letter key worked at all. The typing block is now read from the + scancode; the arrows, the numpad and the F-keys still go by virtual key, and the frame + key (`Input.key`, typing) keeps the layout's character. + - **macOS** did the same through `charactersIgnoringModifiers`; it now reads `keyCode`. + - **`Input.key_label(key)`** names a key for the player in their own layout - `'w'` shows + as "W" on QWERTY and "Z" on AZERTY - so a game that shows its bindings never shows a + key the player cannot find. Windows reads the layout; headless, macOS and the browser + give the US character. +## v0.16.1 — 2026-09-15 + +### Features + +- **HDR calibration, and two HDR fixes** — the HDR10 picture follows the player's display instead of one fixed curve. + + - **`r3d_hdr_calibrate(peak, paper, black)`** — nits as float bits: the brightest the display shows (100-10000), + where the picture's and the interface's white sit (80-1000, never above the peak) and how far the darkest shade is + lifted (0-5, fading out by paper white). The tonemap's and the overlay's HDR10 variants read them, and the display's + HDR metadata is sent again with the new peak. The defaults are the old constants: 1000, 200 and 0. + - **`ov_hdr_nits(nits)` / `ov_hdr_paper()`** — what the overlay draws next is at that many nits on an HDR10 frame, + for a calibration screen's test patches; it closes the batch so far. No effect on an SDR frame. + - **Fixed: a crash toggling HDR on the Vulkan renderer.** `gpu_caps_probe()` made and destroyed a second Vulkan + instance under NVIDIA Streamline's interposer; the next swapchain rebuild then called through a pointer that + instance had left behind, at address 0. With the Vulkan renderer running, the probe asks its instance instead. + - **Fixed: yellow read as red in HDR.** The overlay drew sRGB values straight into the HDR10 swapchain (it now has an + HDR10 variant, chosen while `gpu_hdr_active()`), and the tonemap extended highlights per channel, which boosted a + bright yellow's red far more than its green; it now brightens the graded colour by one factor. +## v0.16.0 — 2026-09-15 + +### Features + +- **A loading screen can show the renderer's start-up as it happens.** `r3d_open(w, h, title)` opens + the window and the graphics backend with nothing baked, and `r3d_load_count()` / `r3d_load_step(i)` + run the rest - the sky and its light, the terrain, the shadow and screen targets, the scattered cover + and actors, the grass - one step at a time, so a game can draw and present a frame between them. + `r3d_init` is those same calls in a row and is unchanged for everything that uses it. +- **HDR10 output on the Vulkan renderer** — `r3d_hdr(on)` asks for an ST 2084 / BT.2020 swapchain where the + display offers one. + + The tonemap's HDR10 variant keeps the SDR picture up to a 200-nit paper white and rolls highlights on to + 1000 nits; the overlay converts the interface to the same white; HDR metadata is set where the loader has + the command. The screen and LDR images are 10-bit while it is on, and screenshots refuse rather than write + a PQ image as SDR. OpenGL and the Vulkan SDR frame are unchanged. `R3D_HDR` overrides the setting. +- **Mesh-shader grass** — on a card with `VK_EXT_mesh_shader`, `r3d_mesh_grass(on)` draws each grass tile as one + mesh-shader dispatch sized to that tile's blades, and a blade that is culled emits no vertices at all. + + - **`ludic-dev shaders`** — a variant whose first file is `*.mesh` compiles that stage as a Vulkan 1.3 mesh + shader; its SPIR-V keeps the `.vert` name, so the manifest is unchanged. + - **The seam** — `gpu_has_mesh()` and `gpu_draw_mesh_tasks(x, y, z)`; a mesh program's pipeline has no + vertex input and its bindings the mesh stage. The device enables `meshShader` and `maintenance4`. + + Not yet faster: at 4K on an RTX 3070 Ti the grass pass takes 5.0 ms against the chunked path's 1.7, so + `gpu_feature_implemented(GF_MESH_GRASS)` stays false and a game should not offer it as a finished setting. +- **More of the renderer's quality is a switch a game can offer.** + + - `sky_set_quality(width)`: the prefiltered sky light at 256, 512 or 1024 wide (half as tall), + baked again at once. + - `post_set_msaa(samples)`: 1, 2 or 4 samples for the scene on OpenGL, remaking the screen targets in + place (`post_msaa_live()` is false on Vulkan, which has no sample counts yet); `post_free` now + frees the multisampled framebuffer too. + - `STREAM_BUDGET_US` is a variable: the microseconds a frame may spend generating streamed cover. + - `r3d_fog_scale`: a multiplier over the fog density the daylight and the weather set. +- **NVIDIA DLSS super resolution and Reflex on the Vulkan renderer** — through NVIDIA Streamline 2.14.1. + + - **The loader** — on Windows `Vk.open()` takes `sl.interposer.dll` beside the executable in place of + `vulkan-1.dll` when a program asks (`Vk.sl_prefer`), falling back to plain Vulkan without it; the `sl*` + exports and feature functions are reachable from Ludic. + - **render3d** — `r3d_dlss(mode)` upscales the lit HDR frame before bloom and the tonemap, jittered on a + Halton cycle, with preset K in every mode (the default M cost 18 ms a frame at 4K on an RTX 3070 Ti); + `r3d_dlss_render_w/h` give the size to render at. `r3d_reflex(mode)` sleeps each frame and marks + simulation, submit and present. `R3D_DLSS`, `R3D_REFLEX`, `R3D_DLSS_PRESET`, `R3D_SL_LOG` for tests. + - **Commands the interposer lacks** — it exports no `vkSetHdrMetadataEXT`, `vkCmdDrawMeshTasksEXT` or + acceleration-structure command; `Vk.has` checks one and `vkGetDeviceProcAddr` reaches it. +- **Real OS threads behind `Job.*` and `Sync.*`** — `Job.parallel_for` runs work across every core. + + - **`fn name`** — an expression naming a top-level function as a value, for a worker's entry point. + No closures: the data a worker needs is passed in. + - **`Job.parallel_for(count, fn work, ctx)`** — `work(i, ctx)` for every `i` in `[0, count)`, split + into chunks across a worker pool (one thread per core but one, pthreads or Win32) and the calling + thread; returns when all are done. `Job.is_worker()` says whether code is on a pool thread. + - **`Sync.*` is thread-safe** — mutexes are real mutexes, atomics are compare-and-swap, channels are + guarded, and `Sync.cpu_count()` reports the machine's cores. + - **A worker must not change the world** — `spawn` and `despawn` on a pool thread stop the program with + a located message. +- **Texture filtering and shadow resolution as settings a game can change while it runs.** + + - `r3d_set_anisotropy(level)` sets the anisotropic filtering of every mipmapped texture already + loaded, not only of later uploads, on OpenGL and Vulkan. + - `shadow_set_res(size)` remakes the cascades' depth layers at 1024, 2048 or 4096 (`shadow_res` + replaces the `SHADOW_RES` constant); the lighting reads the texel size from the map. +- **`app native ""` in `package.ludic`** — `ludic bundle` on Windows copies that directory's files beside + the executable: native libraries loaded at run time (NVIDIA Streamline's DLLs, say) and their licence texts, + which cannot live in the pack. + +### Fixes + +- **The meadow's grass draws on the Vulkan renderer again** — the chunked grass path uploaded its `vec4` tile + table with `u_fv`, which on Vulkan copies one float per array element, so every tile had no blades. + `u_f4v` uploads arrays of `vec4`. + +### Performance + +- **The sky's light is baked once at start, not twice.** A game that turns the sky at boot sets + `sky_start_yaw` before `r3d_init`, and `sky_load` bakes the image-based light at that yaw; turning to + a yaw the light is already baked at (`sky_set_yaw`) no longer bakes it again. Without it, nothing + changes. +## v0.15.0 — 2026-09-15 + +### Features + +- Vulkan for Ludic, and the start of a second renderer beside OpenGL. + + - **`Vk.*`**: every command of Vulkan 1.0-1.4 and of the extensions a modern renderer is + built around (swapchain, HDR colour spaces, ray query and acceleration structures, opacity + micromaps, mesh shaders, variable rate shading, memory budget, pipeline libraries, NVIDIA + low latency, portability for MoltenVK), with every constant and every struct's size + (`_sizeof`) and field offsets (`_`). Generated from the Vulkan + registry by **`ludic-dev vkgen`** into `runtime/native/vk_api.ludic` and `vk_thunks.ll`; + structs are plain memory filled by name with `Vk.put_i32` / `put_i64` / `put_ptr`. Every + size and offset was checked against the SDK's C headers (3128 facts). A C float is float + bits in an int; 64-bit values and non-dispatchable handles are `long`. + - **The loader is opened at run time**, never linked: `vk_win.ll` (`vulkan-1.dll`) and + `vk_mac.ll` (`libvulkan.1.dylib`, MoltenVK). `Vk.open()` returning 0 means no Vulkan, and + the program carries on. `ludicc` and `ludic build` link both files for any program that + uses `Vk.*`. + - `examples/rendering/vk_probe.ludic` reports what a machine's Vulkan can do; + `vk_compute.ludic` runs a Slang compute shader (`vk_compute.slang`) and reads the picture + back - on an RTX 3070 Ti and on an M4 Pro through MoltenVK, clean under the validation layer. + - **render3d `gpu.ludic`**: the seam between the renderer and a graphics API. Render state + and uniforms go through `gpu_*` / `u_*` and nowhere else (OpenGL frames unchanged); + `R3D_GFX=gl|vk` or `gpu_request` chooses a backend, falling back to OpenGL with a reason; + `gpu_caps_probe()` detects, on Windows, the Vulkan 1.3 floor, ray tracing, mesh shaders, + the NVIDIA RTX generation, Reflex and HDR, for a settings screen to grey out what a machine + cannot use (`R3D_CAPS=rtx50|rtx40|rtx30|amd|intel|none` pretends, for tests). +## v0.14.2 — 2026-09-14 + +### Fixes + +- **`Input.mouse_dx` / `mouse_dy` no longer jump on the first frame or when the cursor mode changes.** + The delta was the position minus the previous frame's, and the previous position started at 0,0, + so the first frame reported the cursor's whole distance from the corner as motion, and switching + between a locked cursor's virtual reticle and the real cursor did the same. A camera that adds + `mouse_dy` to its pitch came up pointing at the ground with nobody touching the mouse. Both frames + now report no motion; every other frame is unchanged, windowed or headless, on macOS and Windows. ## v0.14.1 — 2026-09-13 ### Fixes diff --git a/COMPILING.md b/COMPILING.md index 2fd42133..8f03d7d3 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -25,7 +25,7 @@ > ```bash > # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic > mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc -> bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev +> bin/ludicc --unsafe --globals tools/ludic-cli/dev.ludic -o bin/ludic-dev > bin/ludic-dev build # the whole toolchain into bin/ > # (ludicc, ludic, ludic-fmt, ludic-lsp) > bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly @@ -73,6 +73,9 @@ point at a different LLVM toolchain if you have one. | a windowed native executable | `ludicc game.ludic -o build/game` | | a headless executable | `ludicc game.ludic --headless -o build/game` | | the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` | +| the schema an editor reads (records, registries and their entries, consts) | `ludicc src.ludic --emit-schema schema.json` | +| every error, as a JSON array on stdout | `ludicc src.ludic --check --diagnostics=json` | +| the same, with an unsaved buffer on stdin standing for one of its files | `ludicc src.ludic --check --diagnostics=json --stdin-file lib/a.ludic < buf` | | a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` | | a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` | | an object file † | `ludicc src.ludic -c -o src.o` | @@ -350,6 +353,7 @@ The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept: --windowed force a windowed (Cocoa) build --headless force a headless build (stdin input, out.ppm output) --emit-llvm stop at LLVM IR — write it and exit, no clang + --check every check a build makes (types, modules, uses, layers, ports, binds); write nothing --fmt lex + parse only; exit 0 if it parses, 1 on a parse error (the check-docs gate; canonical formatting not yet restored) --save-temps keep the intermediate .ll diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1c8128a..c6d0c971 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ From a clean checkout, one line lifts the toolchain off the seed: ```bash mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc -bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev +bin/ludicc --unsafe --globals tools/ludic-cli/dev.ludic -o bin/ludic-dev ``` That gives you `bin/ludic-dev`, the contributor tool: it replaces every diff --git a/LANGUAGE.md b/LANGUAGE.md index fd04a112..eff6abe7 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -45,7 +45,11 @@ program ChronoRift { ``` `import "emberdepths/*.ludic"` imports every `.ludic` file of a directory, in name -order — a game lists its modules once. An imported file is a **fragment**: bare +order — a game lists its modules once. `import "camp"` names a directory through its +**barrel**, `camp/index.ludic`: a fragment that lists the directory's own imports +(relative to itself), in the order it wants them. A directory with no `index.ludic` +is an error that says so. Packages resolve the same way: `import "ludic.render3d"` +reads `ludic.render3d/index.ludic` from `ludic_modules/` or the toolchain. An imported file is a **fragment**: bare declarations, no `program` wrapper. Its declarations are spliced into the importing program. Imports may appear inside the `program` block or before it, they may nest (a fragment may import fragments), @@ -58,6 +62,1395 @@ parse: chronorift/world.ludic:1: error: expected expression ``` +All of a program's files share one namespace, so **a name is defined once**: two functions, two +`var`s or `const`s (or an `enum` and a `const`), or two `property` / `event` records with one name +are an error that names both places. Declarations the runtime splices in are its own and are not +checked against each other. +The same holds inside a function: a `let` or `var` declares its name once per block (another +block, a loop variable or a parameter may reuse it), and a function with a result type must +`return` one on every path - running off the end of its body is an error, not a zero. + +### Modules (`module`, `export`, `friend module`) + +One namespace is not the same as one room. A barrel that says `module bank` makes its +directory a **module**: the barrel, every file it imports, and every file those import - until +one says `module` of its own - belong to `bank`. Inside a module every name is visible as +before. From anywhere else, a module's `function`, `var`, `const`, `property` or `event` is +reachable only if its declaration says `export`: + +```ludic +# doc-check: skip — a module spans files +# bank/index.ludic +module bank +import "ledger.ludic" + +# bank/ledger.ludic +export state Bank { balance: int = 0 } # its fields are bank's to change: only bank's functions do +export event Deposited { amount: int } +function add(b: mut Bank, n: int) -> void { b.balance += n } # private: only module bank sees it +export function deposit(b: mut Bank, n: int) -> void { + add(b, n) + emit Deposited(amount: n) +} +``` + +A program that imports `bank` may call `deposit` and listen to `Deposited`; calling `add` is + +``` +visible.ludic:5: error: add is private to module bank; mark it 'export' where it is declared (bank/ledger.ludic) +``` + +A file in no module - the program's own file, the runtime - is public, and a package found +through `ludic_modules` or the toolchain keeps its own module rather than its importer's. A +program that says `friend module lab` sees every module's private names: that is for a test +harness, which has to reach inside what it tests. `friend module lab of fishing, data` narrows +that to the modules named: `lab` sees their private names, and every other module's exports only. `export` is a keyword before a declaration +and is not the `@export` annotation, which names a C symbol. + +**A module's private names are its own.** A function, `var` or `const` a module does not export +may share its spelling with a name in another module, in a file in no module, or in the engine's +runtime: `shop` and `weather` may each have a private `seed`, and each module's code reaches its +own (a local of the same name still shadows it). There is no need to prefix a package's privates. +Exported names are one namespace across the program, so two modules that both `export function +seed` are still refused (`function 'seed' is defined twice`). Where two names meet, the private +one is compiled under its module's name (`seed$shop`), which is the spelling a message about it may +show. + +The same holds for a `property` and an `event`: `fishing` and `hunting` may each have a private +`Catch` record and a private `Landed` event with different fields, and each module's types, `new`, +`emit` and `@On` reach its own (compiled as `Catch__fishing`). Two exported ones of one spelling are +refused (`'Catch' is defined twice`, `event 'Landed' is defined twice`). When one of the two is a package's export the message +says so (`'UiNode' is defined twice (...): ludic_ui exports it, and exported names are one namespace - +rename this one, or declare it without export inside a module of your own`); a package exports only +what a program uses, so ludic.ui's `UiAct` is its own and a game's may take the name. Three kinds of record stay +one namespace, because other code names them by spelling: a generic record, a property that is an +entity's component (a `model` names it), and the record a component or a view generates. + +To move an existing codebase onto modules, build it once with `LUDIC_VIS_REPORT=1`: every +reference that would be refused is printed as `vis: :: . used from +` and the build goes on, so a script can add the `export`s the program already relies on. + +### What a module may reach (`uses`) + +`export` says what a module offers; `uses` says what a module takes. A module line may name the +only other modules its files reach: + +```ludic +# doc-check: skip — a module spans files +# fishing/index.ludic +module fishing uses base, data +``` + +From then on a reference from `fishing` into any other module is refused, even to a name that +module exports: + +``` +fishing/land.ludic:4: error: fishing uses items.inv_add (items/index.ludic:3): add 'uses items' to fishing's module line, or take it through a port +``` + +- A module with no `uses` clause keeps the rule before it - anything exported - so the rule can be + switched on one module at a time. Two module lines for one module add their lists together. +- A package's module counts like any other: a mechanic that says `uses ludic_base` and calls + into `ludic_inventory`, `ludic_ui` or the renderer is refused. A package with no `module` line + of its own (`ludic.render3d`) is named for its directory - `ludic_render3d` - for this rule; its + names stay public to the export rule. Only the engine's own runtime - `Value`, `Json`, + `Random` and the rest, which is in no module and no package - needs no naming. +- A file in no module (the program's root) is unaffected either way. +- `module ludic_base uses` with nothing after it is a module that reaches no other module at all. +- A friend of a module (`friend module lab`, or `friend module lab of fishing`) is not held to its + `uses` for that module. +- The declared graph may not go round: `module a uses b` beside `module b uses a` is refused + (`the modules' uses go round in a circle: a -> b -> a`) - one of them takes the other through a + port instead. +- **Layers.** `module flow in layer app uses base, items` puts `flow` in layer `app`. The modules of + one layer use each other freely, without naming each other, and may go round - a game's app + modules (the flow, the menus, the HUD) reach each other by design. Everything outside the layer is + still held to the module's `uses`, and a layered module with no `uses` may reach nothing outside + its layer. A cycle is allowed only inside a layer: `items uses hud` with `hud` in layer `app` + using `items` back is `items -> layer app -> items`, refused. A module is in one layer. +- `LUDIC_VIS_REPORT=1` lists these too, as `uses: :: . used from + (module )`, and builds. + +### Ports (`port`, `bind`) + +A module that needs something from outside itself - the time, a save, a sound - declares a +**port** instead of naming (and `uses`-ing) the module that answers. A port is a record of +function values; a member with no default is required: + +```ludic +# doc-check: skip — a module spans files +# clock/index.ludic +module clock uses units +export port Clock { + now: fn() -> int + day: fn() -> int = fn first_day +} +export function hour_of_day() -> int { return Clock.now() - (Clock.day() - 1) * HOURS } + +# app/index.ludic - where the program is put together +module app uses clock +bind Clock { now: fn game_hours } +function game_hours() -> int { return g_hours } +``` + +A member that takes nothing can be bound to a variable instead: `bind Purse { money: g_money }` for a +`money: fn() -> int` writes the getter (`bind_Purse_money`, returning `g_money` as it is at each call) +in the bind's own file, so the one-line wrapper function is not needed. A member that takes something +is refused a variable (`bind Purse: price is a fn(int)->int, and only a member that takes nothing can +be bound to a variable`). + +Calls go through the port by name, `Clock.now()`. `clock` never names `app`, so it needs no +`uses app`; the binder must be able to see the port - it is `export`ed, and a binder that says +`uses` names the port's module. What the bind names (`fn game_hours`) is checked from the bind's +own file. The compiler refuses: + +``` +app.ludic:5: error: port Clock is used here but never bound: the program has to say 'bind Clock { ... }' once, where it is put together +app.ludic:9: error: bind Clock leaves out now, which has no default +app.ludic:11: error: port Clock is bound twice (first at app.ludic:10) +app.ludic:9: error: port Clock has no member later +``` + +and a member of the wrong type is a type error like any other (`field now of Clock_port wants a +fn()->int and this is a fn(int)->float`). A port nobody uses may stay unbound, and so may a port +whose every member has a default: unbound, it answers with its defaults - which is how a +package's fallbacks ("unbound: this machine runs the world") are written. + +### State: no function writes a global (`state`, `mut`) + +A function that changes a module variable it was not given is a hidden coupling: nothing in its +signature says what it touches, and it cannot run without the whole program around it. So a +module-level `var` is refused, and a module's changing data is a **state** record instead: + +```ludic +# doc-check: skip — a fragment +state Hiker { + hips: int = -1 + spine: int = -1 +} + +function hiker_bind(h: mut Hiker, sk: Skin) -> void { + h.hips = skin_joint(sk, "hips") + h.spine = skin_joint(sk, "spine") +} +function hips_of(h: Hiker) -> int { return h.hips } +``` + +- **One instance, which no code names.** The program holds exactly one of each `state`, made + before any code runs. It reaches code only as a parameter: `h: mut Hiker` may change it, `h: Hiker` + may only read it. So a function's signature is everything it reads and writes, and a test hands + it a plain value. +- **Read-only is checked where it is written.** Through a read-only state the compiler refuses an + assignment whose target starts at it (`h.hips = 1`, `h.list[i] = x`), a `push` onto something in + it, and passing it where a `mut` one is wanted (`bump changes Tally (c: mut Tally), and c is + read-only here`). A reference read out of it into a local (`let l = h.list`, `let r = h.rows[0]`) + is read-only too, so a write through that is refused the same way; a value read out (`let n = + h.count`) is a copy and the local's own. `machine h.mode` writes its store on every `become`. + `mut` is for a state parameter only. +- **The runtime supplies it at the entry points** - the only code nothing in the program calls: + - a body that declares it: `entry (h: mut Hiker) { ... }`, `handler Draw(h: Hiker) phase Render + { ... }`, `@On(Ping) handler Heard(h: mut Hiker) { ... }`, `@OnSpawn(M) handler Made(h: mut + Hiker) { ... }`, a scene's `on enter (h: mut Hiker) { ... }`, `test "name" (h: mut Hiker) { ... }`; + - a retained `ui` block, which names a state's instance by the state's name: `font: Menu.title_font`; + - a component, whose header names its states: `component Tally (score: mut Score) { ... }`; + - a function value: `fn tick` of `function tick(h: mut Hiker, t: Tick)` is `tick` with its + leading states supplied, a `fn(Tick) -> void` - so a system's functions, a port's bind and any + callback a package calls are entry points without saying so; + - a port member bound to a state's field, `bind Purse { money: Wallet.cash }`; + - a call the compiler writes: a namespace method's target (`Weapon.def(...)` of `function + weapon_def(w: mut Weapons, ...)`), an engine system, a runtime built-in. + Every other call passes its states explicitly. +- **Everything else module-level is immutable all the way down.** `let LIMITS: []int = [1, 2]`, + a `const`, a registry: an assignment or a `push` that starts at one is refused, and so is one + through a local that holds part of it (`let r = LIMITS; push(r, 3)`). +- **Tests get fresh states.** Each test block starts from states made new, in its own process under + `ludic test` and in the runner run directly. +- The toolchain's own programs (the compiler, the CLI) are not part of this yet: they build with + `ludicc --globals`, which lets a module-level `var` through. + +The errors: + +``` +counter.ludic:5: error: this assignment: c is read-only here (c: Tally); take it as c: mut Tally to change it +counter.ludic:3: error: a module-level var is refused: a module's changing data is its state (state Name { ... }), passed to the functions that use it - or, if it never changes, a let +counter.ludic:5: error: this assignment: LIMITS is module-level and immutable all the way down; changing data belongs in a state, passed as a mut parameter +counter.ludic:3: error: x: mut int - mut is for a state parameter, and int is not a state +``` + +**`ludic migrate state [file|dir...] [--prune] [--runtime] [--dry-run]` moves programs there.** It compiles +each program and, from the compiler's own view of every name: + +1. a var nothing writes, holding a value (an int, a string, an enum...), becomes a module-level + `let` where it stands; +2. each module's other vars become one state, `state State { ... }`, where the first of + them was - a module's by its name (`module fishing`: `FishingState`), a directory with no module + line by its path (`ludic.render3d`: `Render3dState`), the program's own file by the program's + name (`program SceneDemo`: `SceneDemoState`); the fields keep their comments; +3. every reference to one is rewritten to `_st.` (`scene_demo_st.counter`), and in a + `ui` block to `.`; +4. each function's states - those it touches, and those of everything it calls, to a fixed point - + become its leading parameters, `mut` where it or something it calls writes; a state a run before + declared read-only becomes `mut` where it is now changed; +5. each call passes them on, each entry point declares them (after any it declares already), and + each component's header declares what all its members need. + +Give it every program at once - a directory stands for the test programs under it - and it merges +their plans before it edits anything: programs that share a package agree about it, a module two of +them see different files of is one state, and a path reached as `../../packages/x` is the same file +as `packages/x`. A program's own file keeps a state of its own (named for the program) whatever module it says, and +a `friend module`'s other files go by their directory, since each program declares that module for +itself. A package's var nothing in the given programs writes stays changing data (a game may set +`r3d_dem_path`); only a private one of a package's module becomes a `let`. A name the program uses +that a package migrated earlier moved into its state (`cam_pos`, now `Render3dState`'s) is rewritten +through that state, and a read of the runtime's own var from outside it through the runtime function +that answers it (`gl_w` is `gl_width()`). A program's own module named like a package (a game's `module fishing` beside +`ludic.fishing`) gets a state of its own, `FishingAppState` / `fishing_app_st`, never the package's. +It writes only under the programs and directories it is given (and `runtime/` with `--runtime`): +when the programs need a change in a file anywhere else - an installed package, a module imported +from a neighbouring directory - it says which and changes nothing. It prints what it cannot decide +(a var read in another global's initializer, a reference in generated code), for a person to finish. +A later run finds the states an earlier one made and adds to them. `--runtime` moves the runtime's +own vars too. gpp's packages and examples were moved with one command: + +``` +ludic migrate state packages packages/ludic.lab/example/plate.ludic +migrate: 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files +``` + +**`--prune` takes out what a function no longer uses:** each state parameter that neither the +function nor anything it calls uses, and the argument that fills it at every call. An argument for a +parameter the callee no longer has goes too - a package's verb that dropped a state leaves its callers +passing one too many, and `ludic migrate state --prune ` puts them right. A reducer keeps its +state, and a state declared after a plain parameter (`home_keep(r: Records, save_st: mut Save)`) is +kept where it is. When ludic.base's queues began keeping their own counts, every package verb lost its +`base_st` this way (`wallet_earn(wallet_st, n)`, not `wallet_earn(base_st, wallet_st, n)`): + +``` +ludic migrate state --prune packages packages/ludic.lab/example/plate.ludic +migrate: 0 vars into 0 states, 0 into lets; 1889 edits in 132 files +``` + +### Actions and reducers (`action`, `reducer`, `dispatch`) + +Threading states makes a function's signature say what it touches, and it shows where one function +does everything: an input handler that reads the keys and then changes the world itself takes every +state the world has. An action separates the two. The input says WHAT happened; each module decides +what that means for its own state, and nothing else: + +```ludic +program Pack { + state Bag { + items: []int = new []int + weight: int = 0 + } + state Log { lines: []string = new []string } + action PickUp { item: int, kg: int = 1 } # what happened: a typed record + + reducer Bag on PickUp(b: mut Bag, a: PickUp) { # in the module that owns Bag + push(b.items, a.item) + b.weight += a.kg + } + reducer Log on PickUp(l: mut Log, a: PickUp) { push(l.lines, `picked {a.item}`) } + + handler Keys phase Input { + if Input.key() == 'e' { dispatch PickUp { item: 7 } } # a translator: keys to actions + } +} +``` + +- **`action Name { fields }`** is a record, with defaults like any. `export action` for other + modules to dispatch it. +- **`reducer State on Action(s: mut State, a: Action) { ... }`** WRITES exactly its state, and takes + the action last. Between the two it may declare states it only READS - `reducer Wallet on Buy(w: + mut Wallet, s: Shop, a: Buy)` - supplied like its own, for what is only known inside the drain (a + price another reducer just set, the map in play, where the save lives). A second state to write + is refused (`reducer Pack on Buy: a reducer writes one state, and w is a mut Wallet - read it (w: + Wallet), or dispatch an action Wallet's own reducer takes`), and so is a call inside it to a + function that writes another. What the dispatcher knows rides in the action. Several reducers may + handle one action, one per state; a reducer is not called by name. +- **`dispatch Action { fields }`** queues the action, from anywhere: a handler, a function, a + listener, a reducer. The queue is the runtime's, supplied like an entry point's state, so + dispatching needs no state parameter. +- **When the queue is drained:** at the end of every phase of the frame loop (so what the `Input` + phase dispatches is reduced before `Update`); after every phase of the ludic.base system runner + (`core_tick_all`); when ludic.ui has run a frame's presses (`ui_show`, `ui_press`), so a button's + action is reduced before the host presents the frame, not a frame later; and wherever the program + calls `drain_actions()` (an `entry` program, a test, a loop of its own - and a host that runs UI + presses some other way calls it before it presents). Draining runs the actions in the order they were dispatched, and each action's + reducers in the order of their states' names - never the order of imports - so the same actions + make the same changes on every machine and in a replay. +- **An action a reducer dispatches** is queued behind the rest and reduced in the same drain, never + re-entrantly. A queue still growing after 64 rounds of that stops the program, naming the action: + `actions: Ping is still being dispatched after 64 rounds of reducers - a reducer dispatches what + dispatches it`. +- **Events stay** for what changes no state - a sound, a notice, telemetry - and `@On` listeners run + as the event is emitted. Actions are for changes. + +**A reducer on a row** (27.3). Many of a kind are rows of a ludic.base `Table` (Things, animals, +vehicles), and an action may name ONE of them: its field marked `@Target` holds the row's handle, +and a reducer written `in` the table runs for that row alone. + +```ludic +import "ludic.base" +program Herds { + numbers float + property Deer { + @Column x: float = 0.0 # a table column mirrors it + fear: int = 0 + } + state Herd { deer: Table = null } + state Noise { loud: int = 2 } + action Spook { @Target who: int = -1, by: int = 1 } + action Bolt { @Target who: int = -1, dx: float = 0.0 } + + reducer Deer in Herd.deer on Spook(r: mut Row, n: Noise, a: Spook) { + r.rec.fear += a.by * n.loud + if r.rec.fear > 3 { dispatch Bolt { who: r.h, dx: 5.0 } } + } + reducer Deer in Herd.deer on Bolt(r: mut Row, a: Bolt) { deer_move(r, r.rec.x + a.dx) } + + @RowVerb function deer_move(r: mut Row, x: float) -> void { + r.rec.x = x + tb_set_f(r.tb, 0, r.row, x) # the column and its indexes, with the field + } + entry (h: mut Herd) { + let tb: Table = table_new(1, 0) + h.deer = tb + dispatch Spook { who: tb_add(tb, new Deer), by: 2 } + drain_actions() + } +} +``` + +- **`reducer Record in State.table on Action(r: mut Row, reads..., a: Action)`** - the + table is a field of the state (a path through its records is fine: `WildState.w.tab`), of type + `Table`, and only the module that owns the state declares a row reducer on it. The row + comes first and `mut`, the action last, and between them states it only READS, as any reducer's. +- **`@Target`** marks the action's one field holding the handle (an `int`, what `tb_add` returned). + One to an action: several rows are the dispatcher's loop, one `dispatch` per handle. +- **The drain resolves the handle** (`tb_row`) and hands the reducer a `Row` (ludic.base: + `tb`, `row`, `h`, `rec`) it keeps, one per row reducer and filled in place, so a targeted action + allocates nothing. A handle whose row is gone - the animal removed since the dispatch - runs + nothing, which is not an error; `LUDIC_ACTIONS_LOG=1` prints a line for it. Row reducers take + their place among the action's reducers by their state's name, then the table's. +- **The row goes no further.** Inside a row reducer `r.rec` and `r.h` are all it reaches: `r.tb` + and `r.row` are refused, the view is not assigned, stored, copied or handed to anything but a + **`@RowVerb`** - a function of the record's own module that takes the row first - and a field + marked **`@Column`** (one a table column mirrors: a position, a kind, whether it is alive) is not + written through `r.rec`: `reducer Deer in Herd.deer on Push: Deer.x is mirrored by a column of the + table (@Column) - write it through a @RowVerb, which keeps the column and its indexes with it`. + What `things_verify` catches at run time is a compile error inside a row reducer. +- **Dice** a row reducer rolls come from the row's own `Rng` or its owner's, never `Random.*`: it + runs in the drain, outside its owner's tick, and the world's stream is co-op's shared order. + +**A machine as data** (27.1). A row's state - an animal idling, fleeing, drinking - is an enum field +of its record, and the machine that moves it is a registry marked `@Machine(Record.field)`: one row +a transition, which the compiler turns into the reducers and the tick. The table is the whole +machine, and a studio edits it as a graph. + +```ludic +import "ludic.base" +program Moods { + enum Mood { Calm, Wary, Fled } + property Deer { + mood: Mood = Mood.Calm # the start: the field's default + fear: int = 0 + } + state Herd { deer: Table = null } + action Spook { @Target who: int = -1 } + property DeerStep { + from: Mood = Mood.Calm + to: Mood = Mood.Calm + on: string = "" # an action's name, or "" for a transition the tick asks + guard: fn(Row, Herd) -> bool = null + enter: fn(Row, Herd) -> void = null + } + @Machine(Deer.mood) registry DeerSteps of DeerStep + def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spook", enter: fn deer_startle } + def DeerSteps bolts { from: Mood.Wary, to: Mood.Fled, on: "Spook", guard: fn deer_afraid } + def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_settled } + def DeerSteps home { from: Mood.Fled, to: Mood.Calm } + + function deer_afraid(r: Row, h: Herd) -> bool { return r.rec.fear > 1 } + function deer_settled(r: Row, h: Herd) -> bool { return r.rec.fear == 0 } + function deer_startle(r: mut Row, h: Herd) -> void { r.rec.fear += 1 } + entry (h: mut Herd, m: mut DeerStepsMachine) { + h.deer = table_new(0, 0) + dispatch Spook { who: tb_add(h.deer, new Deer) } + drain_actions() + deer_steps_tick(m, h) + } +} +``` + +- **The registry's record** has `from` and `to` (variants of the field's enum), `on: string` (an + action's name, `""` for a transition the tick asks), and may have `guard: fn(Row, reads...) + -> bool` and `enter: fn(Row, reads...) -> void`, each taking the row first and then the + states it reads. Its rows may live in an `.lres` (`from "deer_steps.lres"`), where a value is + written as in code: `bolts { from: Mood.Wary, to: Mood.Fled, on: "Spook", guard: fn deer_afraid }`. + The states are the enum's variants; the record is held in one state's `Table`, and the + registry is that state's module's. +- **What the compiler writes.** For each action an `on` names (it must have a `@Target`), a row + reducer: the row's current state, the first transition from it on that action - in the table's + order - whose guard passes (no guard passes always), the field set, then `enter`. When a row + leaves a state on a guard alone, `state DeerStepsMachine` (the tick's kept row view) and + `deer_steps_tick(m: mut DeerStepsMachine, s: mut Herd, reads...)`, which takes every row of the + table through the same first match, one transition a row a tick; the program calls it from its + system. A program's own row reducer on the same action runs before the machine's, so an enter + that needs the action's payload finds it on the row. Nothing is allocated: the row views are kept. +- **The table is the whole machine.** Its field is written by nothing else - an assignment or a + `machine` block's `become` anywhere else is refused: `this assignment: Deer.mood is the machine + DeerSteps's (@Machine(Deer.mood)) - it changes only by a transition in its table`. A new row takes + its state in its `new` (a save's load does too). A guard and an enter are functions of the + record's module and keep a row reducer's rules - the row reaches `r.rec` and `r.h`, goes only to a + `@RowVerb` or another of the machine's functions, and a `@Column` field is not written; a guard + asks and writes nothing through its row. +- **The graph is checked**, each an error at its row: a state never reached from the start; a state + with no way out; an `on` naming no action, or an action naming no row; a transition from a state + to itself with no guard; and two ways out of one state on one trigger behind an unguarded first + (`settles and stays both leave Wary on Spook, and settles has no guard - stays could never be + taken`). +- `ludic schema`'s code section lists each machine (`machines`: `registry`, `record`, `field`, + `enum`, `table`, `start`, `states`, `actions`, `tick`, `module`, `at`), and `ludic deps` names its + reducers `reducer Deer in Herd.deer on Spook (machine DeerSteps)`. The `machine` block stays for + a machine that is only code. + +`ludic deps` reports the widest function - the most states any function or entry point of the +program's own takes - and `--check` holds it as a ratchet like its other numbers +(`widest_function 12` in the baseline file). A function value's states are supplied where it is called, so a +step list - `let STEPS = [fn a, fn b]` walked by a function that takes nothing - hides what it +touches; `widest_reach` is the most states any function can come to, through its calls, the `fn f` +it writes and the globals holding fn values it reads (a registry of systems), and the report says how +many of them it does not take itself. `ludic deps --widest N` lists the N functions that take the +most states, each with what it reaches; `--reach N` lists them by reach. + +### Types are checked before anything is emitted + +Between the parse and the emitter a checker walks every function, the entry, the tests, the +globals' initializers and every `@On` listener, and refuses a program whose types do not agree - +all of its mix-ups at once, each at its own line: + +``` +trip.ludic:12: error: metres wants an int and this is a float +trip.ludic:14: error: area takes 2 argument(s) and this call gives 1 +trip.ludic:20: error: + of a string and an int: text joins text only - write string(x) for a number +3 type error(s) +``` + +What it holds apart: `int`, `float`, `fixed` and `bool` (a float or a fixed into an int is +`int(x)`; a float and a fixed never meet but by a literal, which takes whichever kind its slot is); +text and numbers (`string(n)` or a template); one record type and another; slices of different +elements; functions of different types. A call gives exactly as many arguments as there are +parameters, a `return` gives the declared result, and `push` gives the slice's own element. A +function names each parameter once (`add names two parameters n`). A type written in a parameter, a +result or a field names a declared type (or one of the declaration's type parameters): a misspelling +is refused there (`kind_of's parameter f: there is no type CharFact`), not later as a member access +that makes no sense. + +`pointer` is untyped, as `void *` is in C: it goes wherever a reference is wanted and takes any +reference, and a `[]pointer` any slice of references. Restricting what a raw pointer may reach is +`unsafe`'s job, not the checker's. Bits cross between kinds by name - `as_int(x)` / `as_fixed(n)` +reinterpret a word, `float_bits(f)` / `float_from_bits(i)` a float's - never by a slot's type. + +A name the checker cannot type (an engine namespace's arguments, a query's bindings) agrees with +everything, so it only ever reports what it can prove; the emitter keeps its own checks behind +it. `LUDIC_CHECK_REPORT=1` lists every mix-up by category and fails nothing, which is how an +existing program is measured before it has to pass. + +### Generic records and functions + +A record or a function can take type parameters, written after its name: + +```ludic +program Pools { + property Pool { + items: []T = null + n: int = 0 + } + function pool_new() -> Pool { + let p = new Pool + p.items = new []T + return p + } + function pool_add(p: Pool, x: T) -> void { + push(p.items, x) + p.n += 1 + } + function map(xs: []T, f: fn(T) -> U) -> []U { + let out = new []U + var i = 0 + while i < len(xs) { + push(out, f(xs[i])) + i += 1 + } + return out + } + entry { + let names: Pool = pool_new() + pool_add(names, "Crater Lake") + print(names.n) + } +} +``` + +A type names an instance with its arguments - `Pool`, `Pair`, +`Pool>`, `[]Pool` - and two instances of one generic are two types. A call's +type arguments are worked out from its arguments (`pool_add(names, "x")` is `pool_add` at +`string`), a literal deciding only what nothing else did; a call with nothing to say it, like +`pool_new()`, takes them from the slot its result is written into - a `let` with a declared type, +an assignment, an argument, a `return`. Where neither decides, the call is refused and says which +parameter it could not tell. + +Generics are compiled by instantiation: each instance the program uses is an ordinary record or +function, made and checked once, so it costs exactly what writing it out by hand would. A generic +nothing instantiates is not compiled at all. + +### Namespaces declared in Ludic (`alias`) + +A namespace method can be a name for a function. Inside a `namespace` block, + +```ludic +program Trails { + namespace Trail { + export alias length(from, to) = trail_distance + alias km = trail_km + } + function trail_distance(a: int, b: int) -> int { return b - a } + function trail_km(metres: int) -> int { return metres / 1000 } + entry { print(Trail.length(to: 12, from: 2) + Trail.km(metres: 5000)) } +} +``` + +makes `Trail.length(...)` a call to `trail_distance`: the list after the method is the labels a call +may name its arguments by, in the target's parameter order, and without one the target's own +parameter names are the labels (`alias show() = present` takes none). The call is the target's - +same arguments, same checks, same code - so a namespace costs nothing over calling the function. + +This is how the engine declares its own namespaces: `Http`, `Udp`, `Process`, `Json`, `Value`, +`Screen`, `Input`, `Audio`, `World`, `Tiled` and the rest are `alias` blocks in +`runtime/native/namespaces.ludic`, not branches in the compiler, and a package owns an API the same +way in its own files. What is still built into the compiler is the namespaces that compute inline - +`Math`, `Text`, `List`, `Vector`, `Color`, `Time`, `Date` - and the few methods that choose their +target by an argument's type (`Audio.play` of a handle or a name). + +A function of the program's own that is named like the target of one of the engine's namespace +methods would take that method's calls - `Random.range` is `rng_range`, so a package's +`rng_range(a, b, c)` would receive every `Random.range(1, 6)`. Where the program calls that method, +the function is refused (`rng_range is the engine's Random.range, which this program calls (...), +and every such call would reach this function instead; choose another name`), as a function named +like a compiler built-in (`run`, `exit`) or like one of the engine runtime's own functions is. So +is a type - a property, record, enum, event or state - named like one the runtime declares in a file +the program uses: `property PadButton` beside the runtime's `enum PadButton` used to compile and +then fail in clang, where the two layouts met (`PadButton is the runtime's enum +(runtime/native/input.ludic); choose another name for this property`). + +### Registries (`registry`, `def`) + +A table of records that code used to fill with calls in an init function is declared instead: + +```ludic +program Camp { + property Furnishing { + key: string = "" + name: string = "" + cost: int = 0 + } + registry Furnishings of Furnishing as HF + def Furnishings chair { name: "Camp chair", cost: 60 } + def Furnishings crate { name: "Crate", cost: 30 } + entry { print(Furnishings[HF_CRATE].cost + HF_COUNT) } +} +``` + +`registry NAME of RECORD [as PREFIX]` is a global `[]RECORD`, and every `def NAME key { ... }` is one +entry of it - in any file, collected in source order, and in the table before any code runs. Each +entry gets an index constant, `PREFIX_KEY` (the prefix defaults to the registry's name in capitals), +in declaration order, and the registry a count, `PREFIX_COUNT`. When the record has a `key: string` +field it is filled with the entry's key, and `name_find(key)` (the registry's name in lower case) +returns its index or -1. A def's fields are checked against the record like any record literal, a +key is declared once, and `export registry` exports the table, its constants and its lookup. + +Because the index is the order of the defs, a table stored by position - a save, a setting - keeps +the rule it always had: add an entry at the end, never between two. The order is the order the +compiler reads them in, and an `import` is read where it stands: a file's imported defs come before +the defs written after the import line. + +A registry belongs to its module, and a `def` written in another module is refused - unless the +registry says `open`: + +```ludic +# doc-check: skip — a module spans files +# core/index.ludic +module core +export property System { key: string = "", run: fn() -> int = null } +export open registry Systems of System as SY +def Systems clock { run: fn clock_run } + +# weather/index.ludic +module weather uses core +def Systems weather { run: fn weather_run } # weather_run may stay private to weather +``` + +A def into an open registry goes through visibility like any other reference: the registry must be +exported, and a module that says `uses` names the registry's module. What the entry itself names +(`fn weather_run`) is seen from the def's own module. The errors: + +``` +game.ludic:4: error: def Tools saw: registry Tools is not open to other modules; declare it 'open registry Tools' in module kit, or write the def there +game.ludic:4: error: Tools is private to module kit; mark it 'export' where it is declared (kit/index.ludic) +``` + +**The index order of an open registry** is the declaring module's own entries first, in the order +they are read, then every other module's: the modules in the order of their names, each one's +entries in the order they are read. `SY_CLOCK` is 0 however the program imports things, and an +entry from `alpha` comes before one from `zeta` even when `zeta` is imported first - so a table +saved by position keeps its meaning when a barrel's imports are reordered. The rule for a saved +table is still to append: a new entry goes at the end of its own module's list, and a new module +whose name sorts before an existing one moves that one's entries along. A registry whose defs are +all in its own module (or in no module) keeps exactly the order it always had. + +### Resource files (`registry ... from`) + +A registry's entries can live in a data file instead of the source: + +```ludic +# doc-check: skip — the file it names is beside the example +registry Tools of Tool as TL from "data/tools.lres" +``` + +``` +# tools.lres +axe { + name: "Axe" + weight: 1.5 + uses: [{ verb: "Chop", minutes: 20 }, { verb: "Split", minutes: 10 }] +} +lantern { name: "Lantern", weight: 0.75, uses: [] } +``` + +The file is a list of entries, each a key and a record, with `#` comments. It is read when the +program is compiled: the registry's record is its schema, so every entry is checked as a `def` is +- a field the record does not have, a string where it wants a number, a key twice - and the error +names the line in the resource file. A field whose type is a record takes a bare `{ ... }`, and one +typed as a list of records takes `[{ ... }, ...]`; the schema supplies the type. Values are Ludic +expressions in the registry's module, so an entry refers to another table's entry by its constant. +The entries are compiled in: nothing is parsed at start-up, and a build that succeeds has checked +every resource it uses. The path is the project's (where the build runs), else beside the file that +declares the registry. + + +A module that extends an open registry can bring its entries from a resource file of its own: + +```ludic +# doc-check: skip — the file it names is beside the example +import "crafting" # module crafting: export open registry Recipes of Recipe as RC +def Recipes from "recipes.lres" # the game's recipes, after crafting's own +``` + +`def REGISTRY from "file.lres"` reads the file as `registry ... from` does - the project's path, else +beside the file that says it - and checks every entry against the registry's record, an error +naming the resource file's own line (`bad_recipes.lres:3: error: field minutes of Recipe wants an +int and this is a string`). Its entries are defs of the module that wrote the line, so the registry +must be open (and exported) to it, and they take that module's place in the order: the declaring +module's entries, then each other module's by module name, and within a file, file order. + +### Map-scoped tables (`@PerMap`, `@Chunked`) + +A registry can hold what is on a map rather than what is in the game: its rows are not compiled in, +they are read when a map loads, from that map's own directory. + +```ludic +# doc-check: skip — its rows are files under each map's directory +property PropRow { + key: string = "" # the entry's key, as every registry record + @Ref(Models) model: int = 0 # a literal, or any constant by name: MDL_TENT2 + x: float = 0.0 + @Unit("deg") yaw: float = 0.0 + @Ref(Spots) home: string = "" # a row of another map table, by its key +} +@PerMap @ByKey registry Props of PropRow from "props.lres" +@PerMap @Chunked(64) registry Instances of InstRow from "instances/{cx}_{cz}.lres" +``` + +The maps are the directories under the maps root, which `package.ludic` names (`maps "assets/maps"`, +the default; taken relative to the package's directory): `assets/maps/maroon/props.lres`, +`assets/maps/maroon/instances/12_-3.lres`. `from` is relative to the map's directory; in a +`@Chunked(n)` registry, `{cx}` and `{cz}` are the chunk's integer coordinates, `floor(x / n)` and +`floor(z / n)`, written as decimals (`-3`), and they go in the file's own name, not a directory above +it. The files are ordinary `.lres` entries (`key { field: value, ... }`, see Resource files), read +through the same open every asset takes, so a mounted pack serves them and a dev run reads the +directory. A map's row may hold an `int`, a `float`, a `bool`, a `string`, a nested record, a list of +any of those (not a list of lists or of `fn` values), or a `fn` value written `fn name` (resolved +among the functions of the field's type the registry's module can name). An `int` field takes a +literal or any `int` constant by name, a `float` field any number or number constant; the program +carries its constants by name for that, when a `@PerMap` registry exists. The record's first field +is `key: string`, filled from the entry's key. Rows are in file order. + +A `@PerMap` registry has no `as PREFIX` and no `_KEY` constants - its keys are not known when the +game compiles - takes no `def`, and is never `open`. The compiler writes, in the declaring module and +exported with the registry, a `state` named after it and its verbs, prefixed with the registry's name +in snake case (`GroundLayers` -> `ground_layers_`): + +```ludic +# doc-check: skip — what the compiler writes for the two registries above +state Props { rows: []PropRow, map: string, err: string, ... } +props_load(st: mut Props, map: string) -> bool # //props.lres; false + st.err if missing or wrong +props_clear(st: mut Props) -> void +props_find(st: Props, key: string) -> int # the row's index, or -1 +props_path(st: Props, rel: string) -> string # "//", for an asset a row names + +property InstancesChunk { cx: int, cz: int, on: bool, rows: []InstRow, ... } +state Instances { chunks: []InstancesChunk, map: string, err: string, ... } +instances_in(st: mut Instances, map: string, cx: int, cz: int) -> int # its slot; no file is an empty chunk, a wrong one -1 + st.err +instances_out(st: mut Instances, cx: int, cz: int) -> void +instances_slot(st: Instances, cx: int, cz: int) -> int # -1 when that chunk is not in +instances_find(st: Instances, slot: int, key: string) -> int # a row of that slot, or -1 +instances_clear(st: mut Instances) -> void +instances_path(st: Instances, rel: string) -> string +``` + +Read `props.rows[i]` and `len(props.rows)`, `st.chunks[s].rows`. An error is `"file:line:col: what"` +(`maps/alpha/props.lres:2:18: PropRow has no field colour`, `unknown constant MDL_TENT3`), and a +failed load leaves the table empty, never half filled. `_find` is a hash over the row keys, rebuilt +in place on every load: O(1) and allocation-free. `_in` for a chunk that is already in hands back its +slot; `_in` with another map than the one the chunks came from puts every chunk out first. `_path` +makes one string: it is for load time, not a frame. A row's id for co-op is `(map, key)` for a +whole-map table and `(cx, cz, key)` for a chunked one - both stable, because they are data. + +**The table owns its rows, and everything in them.** Every row record, every list inside a row and +every record in such a list is pooled: a reload of the map, or a chunk slot refilled, resets and +refills them in place - the rows list and each row's lists are emptied and refilled, each record set +back to its record's defaults (a template made once) - and a record is made only when a load needs +more of its type than any load before it. Nothing is allocated past that high water, so a frame may +call `instances_in`. Never keep a row, a row's list or a chunk's rows across a load or an `_out`: keep +the key or the index, or copy the numbers. A string field, and a whole-map table's key, is interned +and safe to keep. A CHUNKED table's keys are unique across its map (`t0` .. `t91842`: a row's id is +`(map, key)`, and a tree moved into another chunk keeps it; `ludicc --check` refuses a key written in +two chunk files, naming both), so they are NOT interned - interning every key a player walks past would +fill the bounded intern table and keep them all: a slot's keys live in the slot's own buffers, +rewritten when the slot is refilled. Keep such a key past `_out` with `intern(row.key)`. A record may hold a record of its +own type only through a list. The reader +(the runtime's `lres.ludic`) keeps its own buffers the same way: the file's bytes in one that grows +only for a bigger file, its tree as parallel lists reused from file to file. + +`@Ref(T)` where `T` is a `@PerMap` registry goes on a `string` field holding the row's key (an `int` +field is refused: "use a string key"); its schema attribute says `"scope": "map"`. + +**Every map is checked with the program.** `ludicc --check` (and `ludic build --check`) reads every +directory under the maps root as a map and checks, with the compiler's own resource parser, each +registry's file in it (each file a chunked pattern matches) against the record: the field exists, +its value has the field's type, a constant it names exists, `fn name` names a function of the +field's type, `@OneOf` and `@Range` hold, an `@Ref` into a game registry is in range, and an `@Ref` +into a `@PerMap` registry names a row of that table in the same map (in any of its chunks; `""` is +none). A key is written once in a file. Errors carry the map file's line and column, through the +diagnostics as any other (`--diagnostics=json`); `--no-maps` leaves the maps alone, and no maps +root is nothing to check. `ludic build --check` reads them only for the package's `entry` (the game): +a partial program - a unit test, a molecule, a bake's runner - lacks the game's constants and cannot +judge them, so they are left alone there unless `--maps` asks. `LUDIC_PERMAP_SRC=` appends what the compiler wrote for each table. + +### Editor attributes and the schema (`@Ref`, `@Range`, ..., `ludic schema`) + +A field can say what an editor of the data should offer for it, and a registry how its entries may +change, on the same `@` a field's `@max(64)` is written with - one or several, on the field's line or +the lines above it: + +```ludic +# doc-check: skip — the registries it names are declared elsewhere +property Tool { + @Ref(Vendors) seller: int = 0 # an index into that registry: its entries are offered + @OneOf(GR_) grade: int = 0 # one of the constants whose names start GR_ + @OneOf(GR_GOLD, GR_SILVER) medal: int = 0 # or one of these constants + @Range(0, 20.5) @Unit("kg") weight: float = 1.0 + @Asset("gltf") model: string = "" # a file of that kind (any string) + @Asset("png", map) density: string = "" # a path under EACH map's directory (assets/maps//...) + @Color tint: int = 0 + @Node(model) grip: string = "" # a node inside the glTF that `model` names + @Clip(model) swing: string = "" # a clip inside it + @Material(model) finish: string = "" # a material inside it + @OneOf("box", "hull") shape: string = "" # a string field: one of these words + @Color @Tint(TSLOT_SHELL) shell: int = 0 # a colour for that tint slot + @Derived reach: float = 0.0 # worked out at boot: anything written is overwritten + @Text @Multiline blurb: string = "" # read by the player (so translated), and prose + @Key bind: int = 0 # a key code +} +@AppendOnly @ByKey +registry Tools of Tool as TL from "data/tools.lres" +``` + +`@Unit` takes one of the canonical ASCII spellings - `m`, `m/s`, `m/s2`, `s`, `min`, `h`, `d`, `deg`, +`rad`, `rad/s`, `kg`, `N`, `N.m`, `%`, `px` - so one word means one unit to an editor and a converter: +the angle is `"deg"`, never `"°"`. Any other spelling is a warning naming the canonical one where +there is an obvious one (`"°"`, `"degrees"` -> `deg`, `"sec"` -> `s`, `"m/s^2"` -> `m/s2`, `"Nm"` -> +`N.m`), else listing them; the schema carries the list as `"units"`. + +They change nothing the program does. A target that no part of the program declares - the registry +an `@Ref` names, the constant of an `@Tint` or a listed `@OneOf` - is a warning, and the schema marks +the attribute `"unresolved": true`: a package can name the game's registry without importing it. +Everything else is an error, every one reported: a target that exists but is another kind +(`@Ref(Vendor)` on a record); `@OneOf` of the wrong kind for its field - on a string field the +arguments are words (`@OneOf("box", "hull")`) and every registry row's value must be one of them, on +any other field a prefix ending in `_` or constants; and `@Node(f)`, `@Clip(f)` or `@Material(f)` +naming anything but a field `f` of the same record that is `@Asset("gltf")`, or an `@Ref` to a +registry whose record has exactly one `@Asset("gltf")` field (the model is then the row's). `@Asset(kind, map)` names a file under each map's +directory, not the game's root: `ludicc --check` looks for it in every map - a @PerMap row's in its own +map, a game-wide row's in all of them - and refuses a map that lacks it, unless the field says +`@Asset(kind, map, optional)`; the schema marks it `"scope": "map"`. A plain `@Asset(kind)` is not +looked for. + +`ludicc app.ludic --emit-schema out.json` (or `ludic schema [file] [-o out.json]`) writes what the +compiler resolved, once the types are checked and every open registry has its entries, as one JSON +object with `"schema_version": 1`: + +- `records` - every `property`, `state` and `event`: its module, file, line and column, its doc + comment (the comment lines above it, else the one ending its line), and its fields, each with its + type as text, its default as written (or null), its doc, its place and its attributes + (`[{"name": "Range", "args": [0, 20.5]}]`); +- `registries` - every registry: its record, prefix (null for a `@PerMap` one), resource file, + whether it is open, its `"scope"` (`"map"` for `@PerMap`, else `"game"`), its `"chunk"` size (or + null) and, for a map-scoped one, the `"maps"` root; its own attributes, and its entries in their final index order (`{"key": "axe", "constant": "TL_AXE", + "index": 0, "file": ..., "line": ..., "col": ..., "fields": [{"name", "value", "file", "line", + "col"}]}`), with `contributors`: which resource file or file of `def`s brought which keys in; +- `consts` - every const: its type, its value as written, its module and doc; +- `functions` - every function a `fn` value can name, exported or not: its module, return type, + `params` (the arguments a caller passes), `states` (what the runtime supplies), `signature`, and + `fn_type` - the type a `fn` field sees, the states stripped, spelled as a field's type is + (`fn(NpcPerson,float)->bool`), so matching a function to a field is comparing two strings. +- `components` - every UI component (see Components): its module, place, doc, `xml` and `lss` (the + template's and the stylesheet's paths, `lss` null when it has none); `props` and `state`, the + instance's own fields in the order written, each with its type, its default as written (or null), + its place and doc (a prop's `attributes` are `[]`: a component's members take none); `states_read`, + the program's states its header names, which the runtime supplies and the template never sees; + `derived`, the fields worked out each frame, with their types (an inferred one resolved); + `functions` (`{"name", "params": [["i", "int"]], "result"}`) and `events` (its `on` handlers, + `{"name", "params"}`) as the template calls them, the header's states and the instance stripped; + and `natives`, the registered native tags its template uses as elements; +- `natives` - every `ui_native` / `ui_native_input` call whose tag is a string literal: + `{"tag", "via", "handler", "module", "file", "line", "col"}`, `via` the function called and + `handler` the `fn` value it passes last (else null). A tag worked out at run time cannot be known + and is left out. +- `units` - `@Unit`'s canonical spellings. +- `lang` - the text keys and the languages (see Text keys), or null without a `lang` line. +- `code` - the program's code map, so an editor needs no scan of its own. Every place is + `"file:line:col"` (`"at"`); what the runtime declares and what the compiler writes itself are left + out. It holds: + - `modules` - `{"name", "package", "layer", "uses", "uses_at", "friend", "files"}`: `uses` null + without a `uses` line, `friend` null, or `{"of": null}` for a friend of every module and + `{"of": [...]}` for `friend module lab of a, b`; `files` its source files and the resource files + read into its registries; + - `states` (`{"name", "module", "at"}`), `actions` (`{"name", "module", "at", "target"}`, + `target` its `@Target` field or null), `reducers` (`{"state", "action", "module", "at"}`, a + state's), `row_reducers` (`{"record", "table", "state", "action", "target", "predicted", "net", + "module", "at"}`: `table` the path as written, `State.field`; `predicted` false and `net` null + until `@Predicted` and `@Net` reach reducers), `row_verbs` (`{"name", "record", "module", "at"}`) + and `dispatch` (every `dispatch`: `{"action", "module", "at"}`); + - `events` - `{"name", "module", "at", "cancellable", "net"}` (`net` `"toserver"` for + `@ToServer`, `"toclients"` for `@ToClients`, else null), its `emits` (every `emit`'s place) and + its `listeners` (the `@On` handlers: `{"handler", "module", "at"}`); + - `ports` - `{"name", "module", "at", "bound", "members"}`, each member `{"name", "type", "default", + "required"}` (its `fn` type as a field's is spelled, its default as written); and `binds` - one + row per member a `bind` gives: `{"port", "member", "fn", "value", "module", "at"}`, `fn` the + function a `fn name` names (null for a variable bound to a member that takes nothing) and + `value` as written; + - `handlers` - `{"name", "module", "at", "phase", "hook", "target", "public", "net", "queries", + "scene", "layer"}`: `phase` null for a hook, `hook` null or `"On"`, `"OnSpawn"`, `"OnDespawn"`, + `"OnAttach"`, `"OnDetach"`, `"OnEnable"`, `"OnDisable"`, `"OnStart"`, `"OnQuit"` with `target` + the event, model or property it names; `net` `"server"` (`@Server`), `"predicted"` + (`@Predicted`) or null; `queries` null or `{"these": [{"property", "filter"}], "on"}` (a + filter as written, or null); a scene's handler has its name as written and its `scene` and + `layer`; + - `models` (`{"name", "module", "at", "owned", "properties": [{"name", "sync"}]}`), `prefabs` + (`{"name", "model", "module", "at", "components": [{"property", "fields": [{"name", + "value"}]}]}`) and `scenes` (`{"name", "module", "at", "start", "public", "shows", "lasts", + "then", "loads", "enter", "exit", "layers"}`: `lasts` as written, `then` the scene it goes on to, + `loads` the scene a `loads then` one goes on to, `enter` / `exit` whether it has the block); + - `fn_refs` - every `fn name` written, in code or in a resource file: `{"fn", "module", "at", + "slot_kind", "slot", "entry"}`, the slot it fills: `"registry"` (`"Registry.field"`, `entry` the + row's key), `"port"` (a bind's `"Port.member"`), `"port_default"`, `"default"` (a record + field's default, `"Type.field"`), `"record"` (a `new` or a spawn's `"Type.field"`), `"event"` + (an `emit`'s `"Event.field"`), `"arg"` (`"callee(i)"`, the i-th argument written), `"assign"` + (`s.tick = fn f`: `"s.tick"`), `"let"` (the name), or `"value"` with a null slot. + + Named lists are sorted by name (then file and line), sites by place. + +Each list is sorted by name (then file and line), a registry's entries are in index order, and the +paths are the ones the compiler read, so two runs over the same source write the same file. The +engine's runtime is left out. + +`Build.schema_hash()` is a `long`: FNV-1a 64 of exactly those bytes, for the program being built, +worked out once and only when the program names it - so a tool talking to a running build can tell +whether it was compiled from the data in front of it. It is 0 in a release (`ludicc --release`, +which `ludic bundle` passes). + +### Text keys (`Key`, `k"..."`, `lang`) + +A text the player reads is named by a KEY, and the key's English is a language file like any +other's. `package.ludic` says where the languages are and which is the source: + +``` +lang "assets/lang" en # the directory, and the source language: assets/lang/en.po +``` + +In code a key is a literal of the builtin type `Key` - `k"module.purpose"`, or `kn"..."` for a key +whose text has plural forms - written against its quote (`k "x"` is a name and a string, as ever): + +```ludic +# doc-check: skip — tr / trf / trn are ludic.i18n's +let title = tr(k"pause.resume") # tr(key: Key) -> string +let day = trf(k"hud.day", txt_num(n)) # the English's holes {1}..{4}, in order +let got = trn(kn"catch.count", n, fish) # {1} is the count, the rest follow +``` + +A `Key` is not a `string`, and a `string` is not a `Key`: giving one where the other is wanted is an +error either way, and so is `+` on a key. Keys compare with `==` and `!=`, and are fields, parameters, +results, list elements and registry values like any other type (`null` is none). At run time a key is +its text after a marker byte - byte 1, or 2 for `kn"..."` - so `k"pause.resume"` is the string +`"\x01pause.resume"`: the runtime's `tr` is a cast, and a translator knows a key from English. +`string(key)` gives that marked text, for a runtime that needs it; a program itself makes text with +`tr`, and a key in a template literal's hole (`` `at {k}` ``) is an error - write `trf(k"...", ...)`. +`trn` takes a plural key and `tr` / `trf` refuse one, with or without a `lang` line. (A program that +declares its own type named `Key` keeps it; the builtin is then out of reach.) + +**A package's own words** are keys too. ludic.ui's key field says "Right click" as `ui_tk(ui_st, +k"ui.right_click", "Right click")`: translated through the program's translator when one is bound, the +plain text otherwise (a program with no languages). A program with a `lang` line that imports ludic.ui +therefore carries `ui.right_click`, `ui.middle_click`, `ui.left_click` and `ui.press_key` in its +source `.po`, and the check says so if it does not. + +**The data.** A registry's `@Text` field is text by derivation: its key is +`..` - the registry's name in snake case (`Items` -> `items`, `GearKinds` -> +`gear_kinds`), a list field adding `.` and a nested record `.` - or, with +`@TextKey("steps")` on the registry, `steps..`. A `@PerMap` table's is +`maps....`. When the field's type is `Key` and a row of a compiled registry +gives it no value, the compiler fills in the derived key (marked, as a literal is), so code writes +`tr(Items[i].name)` and the `.lres` never spells a key. The same holds below the row: a nested +record's field adds `.` and a list item `.` (`disruptions..arrive.steps.0.text`), and a +`@Text []Key` a row leaves out takes `<...>.0`, `.1`, ... for as many as the source `.po` has; `field: +null` is no text, and is not checked; a row may still name one, `name: +k"items.lamp.name"`, in a resource file or a map's file alike. + +```ludic +# doc-check: skip — the data file is the game's +property Item { + key: string = "" + @Text name: Key = null # items..name, filled in when the row gives none +} +registry Items of Item as IT from "data/items.lres" +@TextKey("steps") registry StepKinds of StepKind from "steps.lres" # steps.. +``` + +**The templates.** A component's text is a key too: `{t('pause.resume')}`, `title="{t('pause.close')}"`, +`{t('hud.day', day)}`; `t(expr)` takes a key worked out at run time (a `Key` field reaches a template +as its marked text). Words outside an element with `translate="no"` are English still waiting for a +key. + +**The checks.** With a `lang` line and its source `.po` there, the compiler holds every key to it, +each diagnostic at its `file:line:col`: + +- a `k"..."` literal, a template's `t('...')` literal, or a key a data row names or derives that the + source `.po` does not have is an **error**; so is a `kn"..."` whose entry has no `msgid_plural`; +- a call of `trf` or `trn` with a key literal, and a template's `t('key', ...)`, gives as many values + after the key as the English's highest hole `{n}` (`trn`'s count is `{1}`; trailing `""` literals + are padding and not counted), or it is a **warning**; +- **English left** - a template's words outside `translate="no"`, a text attribute's words (`title`, + `label`, `hint`, `text`, `caption`, and any other whose value is not a keyword), a quoted choice + inside a hole that reads as words, and a `@Text` row whose value is still English - is an + **error** (a warning until the migration was done; `ludic deps` still counts it as `english_left`); +- one English under several keys (a split) wants a `#.` description on each, for a translator to + tell them apart: a **warning** at the `.po`'s line. + +With no `lang` line nothing is checked and nothing is said (a package test uses key literals freely); a +`lang` line whose source `.po` is missing checks nothing, and the first key literal says so once. +The source `.po` is gettext's: `msgid` (the key), `msgid_plural` and `msgstr[n]`, `#,` flags +(`fuzzy`), `#.` descriptions, `msgctxt` (read and ignored: the key is the context), strings over several lines; `#~` +entries are obsolete. `ludic schema` carries a `"lang"` object (null without a `lang` line): every +key used with its kind (`code`, `template`, `data`), its sites, its English, whether it is a plural, +and its description; `unused` (the source's keys used nowhere), `undescribed`, `split`, and per other +language in the directory its `missing`, `fuzzy` and `extra` keys. + +### Default parameters, and calls that name what they change + +A parameter can have a default, and a call leaves out what it does not change - the last ones when +it passes arguments by position, or any it does not name. A call can pass its first arguments by +position and the rest by name: + +```ludic +program Boxes { + numbers float + function box(label: string, w: float = 300.0, pad: float = 8.0, bold: bool = false) -> string { + return `{label} {w} {pad} {bold}` + } + entry { + print(box("a")) # every default + print(box("b", 120.0)) # the first two by position + print(box("c", bold: true)) # the subject by position, a prop by name + } +} +``` + +A default is an expression written with the function and evaluated for each call that leaves it +out. A call that leaves out a parameter with no default, names one the function does not have, or +puts a positional argument after a named one is refused with that said. + +### Components and templates (`ludic.ui`) + +A UI is components, and a component is three files side by side: +- `Name.ludic` declares what it takes, keeps and does; +- `Name.xml` is its HTML-shaped template; +- `Name.lss` holds its CSS-shaped styles, which apply to its own elements only. + +```ludic +# doc-check: skip — a fragment of a program that imports ludic.ui +component Counter { + prop label: string # its parent passes it: + prop step: int = 1 # with a default + state count: int = 0 # the instance's own, kept while it is on screen + doubled: int = count * 2 # worked out every frame + function big() -> bool { return count > 3 } # callable from the template + on bump() { count += step } # an event: on-click="bump()" +} +``` + +- **Props and state** are plain names in the component's code; each mounted instance is a record + of them. A prop or state is an `int`, a `float`, a `bool`, a `string` or a `Val`. A field is + anything a template can read, and records and lists become objects and lists. +- **The template** has one root (use `` for several). A component is used by its name as + a tag. `set count = 0` in an action sets its state, and `emit close` runs what its parent passed + as `on-close`. `class`, `style` and `id` on a component's tag land on its root element, styled by + the parent's sheet as well as its own; when that root is itself a component, every user in turn + passes theirs down (the outermost's `id` wins). The rules of all those sheets that match the root + are weighed together by specificity, as one sheet's are, and a user's rule wins a tie. +- **A component's names.** An event may be called anything, `set` included (`on set(v: int)`, + pressed as `set(4)`; `set x = ...` is still the action). A `string` prop given a number reads it + as text (`label="{3}"` is `"3"`). Two components of one name are an error that names both files. +- **Compiled in.** The compiler reads the template and the styles from beside the declaration and + inlines each `@import` (a path starting with `/` is from the project's root), so a missing + template fails the build and nothing has to ship beside the program. `ui_reload()` reads the + files again when they change on disk and keeps every instance's state. +- **A screen is a component** with no props: `ui_show("Counter", null, x, y, w, h)`, or + `ui_nodes("Counter", null)` for a test. +- **States in the header.** A component that reads the program's data names the states it reads + and changes after its name, as an entry point does: + + ```ludic + # doc-check: skip — a fragment of a program that imports ludic.ui + component Tally (score: mut Score, look: Look) { + total: int = score.points + function big() -> bool { return total > look.big } + on add(n: int) { score.points += n } + } + ``` + + Every getter, default, function and event takes them before the instance; a member's call to + another passes them on; ludic.ui's calls into the component are given the instances; and the + template never sees them - `on-click="add(5)"` passes only `5`. A header state without `mut` is + read-only in every member. + +An older, lighter bridge remains: `view Name { ... }` gives a whole template file of ``s +and ``s (loaded with `ui_load`) one model and one call, and the rest of this section +applies to both: + +```xml + + + + + The purse: ${purse} + + + + + + Nothing bought yet + {owned} bought + + +``` + +- **Elements.** A template is HTML-shaped: + - `div`, `section`, `header`, `footer`, `nav`, `main`, `article`, `aside`, `ul`, `ol`, `li` and + `form` are boxes laid out in a column; `row` is one laid out in a row. + - `p`, `span`, `label`, `h1`-`h6`, `strong`, `em`, `small`, `b`, `i` and `a` are text; words + inside a box become a text of their own. + - `button`, `img src`, `hr` and `spacer`; `scroll` is a column that scrolls. + + A default stylesheet, like a browser's, sizes the headings and pads the buttons. +- **Attributes.** `id`, `class` (which may be bound: `class="{picked ? 'on' : ''} row"`), + `style="padding: 4px; color: gold"`, `hidden`, `disabled`, `onclick` or `on-click` (also + `on-press`), and any attribute a property is named after (`width="300"`). Any other attribute is + kept for `[attr=value]` selectors, as HTML's are. +- **The box model and flex.** Sizes are border boxes. + - `padding` and `margin` take one to four lengths, or one side by name (`padding-left`). + - `border` is `2px solid #ffcc00`, or `border-width` and `border-color`. + - A length is `12`, `12px`, `50%`, `fit`/`auto`, `fill` or `calc()`. `width: 0` and `height: 0` + are 0, not unset. + - `calc()` takes `+`, `-`, `*` and `/` and brackets over `px`, `em`, `rem`, `vw`, `vh`, plain + numbers and `var()`; in a `width`, `height`, `top`, `right`, `bottom` or `left` it may also hold + a percentage of the room or of the containing block (`calc(50% - 10px)`). `min()`, `max()` and + `clamp(least, want, most)` pick among lengths, inside `calc()` or on their own + (`width: min(300px, 50vw)`); a percentage cannot be compared there. + - `top`, `right`, `bottom` and `left` take a percentage of the containing block: its width for + left and right, its height for top and bottom. + - `flex-grow` (or `flex`) shares out the spare room along `flex-direction`, and `fill` is a share + of 1. `justify-content` takes `flex-start`/`start`, `center`, `end`, `space-between`, + `space-around` or `space-evenly`. + - `align-items` and `align-self` take `start`, `center`, `end` or `stretch`. `flex-wrap: wrap` + breaks a row into lines, and `min-`/`max-width`/`-height` bound it. + - `flex-shrink` gives up room when a line's children want more than it has, each in proportion + to its shrink times its size, and none below its min size, a fixed size or its content. A scroll + box shrinks (and scrolls) and a box in a column shrinks as far as the scroll boxes in it let it, + so a list in a column takes the room its siblings leave with no height of its own. `flex: 1 0` + is the grow and the shrink. `order` rearranges a box's children without touching the tree. + - `gap`, `text-align`, `display: none`, `background(-color)`, `color`, `opacity` and `font-size` + are CSS's. + + Every property also has a short name (`w`, `h`, `pad`, `bg`, `size`, `grow`, `align`, + `justify`, `self`, `dir`, `wrap`, `alpha`). Rounded corners, font weight and a few more are + things the renderer does not draw, and the runtime says so rather than ignoring them. A colour + is the renderer's name for one, `#rrggbb` or `#rgb`. +- **Stylesheets.** Rules go in a `

a

b

c

" + function at(ui_st: mut UiState) -> string { + ui_show(ui_st, "a", view_anim(), 0.0, 0.0, 200.0, 200.0) + let r: UiNode = ui_nodes(ui_st, "a", view_anim()) + return `{int(r.children[0].alpha * 100.0)} {int(r.children[1].alpha * 100.0)} {int(r.children[2].alpha * 100.0)}` + } + entry (ui_anim_st: mut UiAnimState, ui_st: mut UiState) { + ui_load_text(ui_st, PAGE, "a.xml") + var out = at(ui_st) + for i in 0 .. 29 { at(ui_st) } + out = out + " | " + at(ui_st) + ui_anim_st.dim = true + for i in 0 .. 29 { at(ui_st) } + out = out + " | " + at(ui_st) + print(out) + } +} diff --git a/examples/library/ui_box.ludic b/examples/library/ui_box.ludic new file mode 100644 index 00000000..2f8f29ff --- /dev/null +++ b/examples/library/ui_box.ludic @@ -0,0 +1,19 @@ +# ui_box.ludic — what a host reads from outside the interface: ui_scale(), the scale it is drawn at +# (the renderer's), and ui_box("id"), where an element was laid out when its screen was last shown, +# in screen pixels (null for one that is not there) - to frame a 3D view beside a panel. +import "ludic.ui" +program UiBox { + numbers float + const PAGE: string = "

title

" + function scale() -> float { return 1.5 } + entry (ui_st: mut UiState) { + let b = new UiBackend + b.scale = fn scale + ui_backend(ui_st, b) + ui_load_text(ui_st, PAGE, "b.xml") + ui_show(ui_st, "b", null, 20.0, 30.0, 800.0, 600.0) + let p = ui_box(ui_st, "panel") + let none = ui_box(ui_st, "nothing") + print(`scale {ui_scale(ui_st)} | panel {int(p.x)},{int(p.y)} {int(p.w)}x{int(p.h)} | missing {none == null}`) + } +} diff --git a/examples/library/ui_boxes.ludic b/examples/library/ui_boxes.ludic new file mode 100644 index 00000000..7242d520 --- /dev/null +++ b/examples/library/ui_boxes.ludic @@ -0,0 +1,25 @@ +# ui_boxes.ludic — ludic.ui's flex and sizes: a scroll box in a column shrinks to the room its +# siblings leave (flex-shrink) and scrolls what it cannot show; text in a row wraps in the room left +# after the icon beside it; width and height 0 are 0; calc() mixes a percentage with lengths; order +# rearranges a row; and a range's track is a row, its fill as tall as the track. +import "ludic.ui" +import "ui_boxes_parts/Boxes.ludic" +program UiBoxes { + numbers float + entry (ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + ui_show(ui_st, "Boxes", null, 0.0, 0.0, 400.0, 600.0) + let root: UiNode = ui_nodes(ui_st, "Boxes", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 600.0) + let page = root.children[0] + let list = page.children[0].children[1] + let line = page.children[1] + let text = line.children[1] + let zero = page.children[2] + let half = page.children[3] + let ord = page.children[4] + let track = page.children[5].children[0] + let fill = track.children[0] + print(`list {int(list.ch)} of {int(list.content_h)} | text at {int(text.x)} {int(text.cw)} wide, {len(text.lines)} lines, row {int(line.ch)} | zero {int(zero.cw)}x{int(zero.ch)} | half {int(half.cw)}x{int(half.ch)} pad {int(half.pt)} {int(half.pr)} | order {ord.children[0].text}{ord.children[1].text}{ord.children[2].text} at {int(ord.children[2].x)} | fill {int(fill.cw)}x{int(fill.ch)} of {int(track.cw)}x{int(track.ch)}`) + } +} diff --git a/examples/library/ui_boxes_parts/Boxes.lss b/examples/library/ui_boxes_parts/Boxes.lss new file mode 100644 index 00000000..30bfda09 --- /dev/null +++ b/examples/library/ui_boxes_parts/Boxes.lss @@ -0,0 +1,14 @@ +.page { width: 300px; gap: 4px } +.panel { height: 200px; gap: 10px } +.bar { height: 30px } +.list { overflow: auto } +.item { height: 20px } +.line { width: 200px; gap: 8px } +.line img { width: 24px; height: 24px } +.zero { width: 0; height: 0 } +.half { --gap: 10px; width: calc(50% - var(--gap)); height: calc(2 * var(--gap) + 1em); padding: calc(1em / 4) 2px } +.ordered { gap: 0 } +.a { order: 1 } +.b { order: 2 } +.c { order: 3 } +.ui-track { height: 12px; padding: 2px } diff --git a/examples/library/ui_boxes_parts/Boxes.ludic b/examples/library/ui_boxes_parts/Boxes.ludic new file mode 100644 index 00000000..e48029a5 --- /dev/null +++ b/examples/library/ui_boxes_parts/Boxes.ludic @@ -0,0 +1,5 @@ +# Boxes.ludic - a panel with a list that takes the room its header and footer leave, a line of text +# beside an icon, zero sizes, calc(), order, and a range's track +component Boxes { + state rows: int = 10 +} diff --git a/examples/library/ui_boxes_parts/Boxes.xml b/examples/library/ui_boxes_parts/Boxes.xml new file mode 100644 index 00000000..1f25e02c --- /dev/null +++ b/examples/library/ui_boxes_parts/Boxes.xml @@ -0,0 +1,12 @@ +
+
+

Head

+

{r}

+

Foot

+
+

A line long enough that it has to wrap beside the icon

+

hidden

+
+ cab + +
diff --git a/examples/library/ui_calc.ludic b/examples/library/ui_calc.ludic new file mode 100644 index 00000000..7a4eea33 --- /dev/null +++ b/examples/library/ui_calc.ludic @@ -0,0 +1,20 @@ +# ui_calc.ludic — min(), max() and clamp() (on their own and inside calc()), and top, right, bottom +# and left as percentages and calc() of the containing block, for absolute and relative elements. +import "ludic.ui" +program UiCalc { + numbers float + const STYLE: string = "" + const BODY: string = "
" + entry (ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + ui_load_text(ui_st, "" + STYLE + BODY + "", "c.xml") + ui_show(ui_st, "c", null, 0.0, 0.0, 400.0, 600.0) + let r: UiNode = ui_nodes(ui_st, "c", null) + ui_place(ui_st, r, 0.0, 0.0, 400.0, 600.0) + let box = r.children[4] + let p = box.children[0] + let q = box.children[1] + let rel = box.children[2] + print(`min {int(r.children[0].cw)} max {int(r.children[1].cw)} clamp {int(r.children[2].cw)} {int(r.children[3].ch)} | p {int(p.x - box.x)},{int(p.y - box.y)} q {int(q.x - box.x)},{int(q.y - box.y)} r {int(rel.x - box.x)}`) + } +} diff --git a/examples/library/ui_component.ludic b/examples/library/ui_component.ludic new file mode 100644 index 00000000..cac2a70b --- /dev/null +++ b/examples/library/ui_component.ludic @@ -0,0 +1,37 @@ +# ui_component.ludic — components as files: Counter.ludic declares props, state, a field, a function +# and an event; Counter.xml is its template; Counter.lss its scoped styles (which @import a theme). +# Two counters keep their own state, a parent's class reaches the child's root, and the child's +# `p { color }` does not leak into the parent. +import "ludic.ui" +import "ui_component_parts/Counter.ludic" +import "ui_component_parts/App.ludic" +program UiComponent { + numbers float + function texts(n: UiNode, out: []string) -> void { + if n.text != "" { push(out, n.text) } + for i in 0 .. len(n.children) { texts(n.children[i], out) } + } + function frame(ui_st: mut UiState) -> UiNode { + let root: UiNode = ui_nodes(ui_st, "App", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + entry (ui_st: mut UiState) { + var root = frame(ui_st) + let b = root.children[0].children[2] + ui_press(ui_st, b.children[1]) + ui_press(ui_st, b.children[1]) + ui_press(ui_st, root.children[0].children[1].children[1]) + root = frame(ui_st) + let out = new []string + texts(root, out) + var joined = "" + for i in 0 .. len(out) { joined = joined + out[i] + "|" } + print(joined) + let b2 = root.children[0].children[2] + print(`b margin {int(b2.mt)}; b text {b2.children[0].fg}; outside {root.children[0].children[3].fg}; button {int(b2.children[1].ch)}`) + ui_press(ui_st, b2.children[2]) + root = frame(ui_st) + print(root.children[0].children[2].children[0].text) + } +} diff --git a/examples/library/ui_component_parts/App.lss b/examples/library/ui_component_parts/App.lss new file mode 100644 index 00000000..29c72305 --- /dev/null +++ b/examples/library/ui_component_parts/App.lss @@ -0,0 +1 @@ +.wide { margin: 9px } diff --git a/examples/library/ui_component_parts/App.ludic b/examples/library/ui_component_parts/App.ludic new file mode 100644 index 00000000..788d54e7 --- /dev/null +++ b/examples/library/ui_component_parts/App.ludic @@ -0,0 +1,4 @@ +# App.ludic — a screen is a component too: this one has no props, only what it shows +component App { + title: string = "counters" +} diff --git a/examples/library/ui_component_parts/App.xml b/examples/library/ui_component_parts/App.xml new file mode 100644 index 00000000..afd44365 --- /dev/null +++ b/examples/library/ui_component_parts/App.xml @@ -0,0 +1,6 @@ +
+

{title}

+ + +

outside

+
diff --git a/examples/library/ui_component_parts/Counter.lss b/examples/library/ui_component_parts/Counter.lss new file mode 100644 index 00000000..15174c36 --- /dev/null +++ b/examples/library/ui_component_parts/Counter.lss @@ -0,0 +1,3 @@ +@import "theme.lss"; +.counter { padding: 4px } +p { color: #ff0000 } diff --git a/examples/library/ui_component_parts/Counter.ludic b/examples/library/ui_component_parts/Counter.ludic new file mode 100644 index 00000000..8a677ed8 --- /dev/null +++ b/examples/library/ui_component_parts/Counter.ludic @@ -0,0 +1,9 @@ +# Counter.ludic — a component: what it takes (props), keeps (state), works out and does +component Counter { + prop label: string + prop step: int = 1 # how much one press adds + state count: int = 0 + doubled: int = count * 2 + function big() -> bool { return count > 3 } + on bump() { count += step } +} diff --git a/examples/library/ui_component_parts/Counter.xml b/examples/library/ui_component_parts/Counter.xml new file mode 100644 index 00000000..617de5aa --- /dev/null +++ b/examples/library/ui_component_parts/Counter.xml @@ -0,0 +1,5 @@ +
+

{label}: {count} ({doubled}){big() ? ' big' : ''}

+ + +
diff --git a/examples/library/ui_component_parts/theme.lss b/examples/library/ui_component_parts/theme.lss new file mode 100644 index 00000000..c7297243 --- /dev/null +++ b/examples/library/ui_component_parts/theme.lss @@ -0,0 +1,2 @@ +/* theme.lss - shared by whoever imports it */ +button { height: 30px } diff --git a/examples/library/ui_controls.ludic b/examples/library/ui_controls.ludic new file mode 100644 index 00000000..7e4508a7 --- /dev/null +++ b/examples/library/ui_controls.ludic @@ -0,0 +1,71 @@ +# ui_controls.ludic — ludic.ui's own controls, driven as a player would drive them: Tab and the +# arrows move the focus, Enter presses and flips, the arrows move a range and step a select, typing +# fills a text field and Backspace takes a letter back, a key field takes the next key, the pointer +# drags a range, and the wheel scrolls a box. Every change reaches the component as an event. +import "ludic.ui" +import "ui_controls_parts/Settings.ludic" +program UiControls { + numbers float + function frame(ui_st: mut UiState, i: UiInput) -> UiNode { + ui_input(ui_st, i) + ui_show(ui_st, "Settings", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "Settings", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + function press(tab: bool, down: bool, right: bool, enter: bool) -> UiInput { + let i = new UiInput + i.tab = tab + i.down_key = down + i.right = right + i.enter = enter + return i + } + function state(root: UiNode) -> string { + let page = root.children[0] + let last = page.children[len(page.children) - 1] + return last.text + } + entry (ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + var root = frame(ui_st, new UiInput) + root = frame(ui_st, press(true, false, false, false)) + root = frame(ui_st, press(false, false, false, true)) + root = frame(ui_st, press(false, true, false, false)) + root = frame(ui_st, press(false, false, false, true)) + root = frame(ui_st, press(false, true, false, false)) + root = frame(ui_st, press(false, false, true, false)) + root = frame(ui_st, press(false, false, true, false)) + root = frame(ui_st, press(false, true, false, false)) + root = frame(ui_st, press(false, false, true, false)) + root = frame(ui_st, press(false, true, false, false)) + let typed = new UiInput + typed.typed = "lovelace" + root = frame(ui_st, typed) + let back = new UiInput + back.backspace = true + root = frame(ui_st, back) + root = frame(ui_st, press(false, true, false, false)) + root = frame(ui_st, press(false, false, false, true)) + let k = new UiInput + k.key = 70 + root = frame(ui_st, k) + print(state(root)) + let range = root.children[0].children[2] + let track = range.children[1] + let at = new UiInput + at.x = track.x + track.cw * 0.8 + at.y = track.y + 2.0 + at.down = true + root = frame(ui_st, at) + root = frame(ui_st, new UiInput) + let list = root.children[0].children[6] + let w = new UiInput + w.x = list.x + 5.0 + w.y = list.y + 5.0 + w.wheel = -1.0 + root = frame(ui_st, w) + root = frame(ui_st, new UiInput) + print(`{state(root)}; scrolled {int(root.children[0].children[6].scroll_y)}`) + } +} diff --git a/examples/library/ui_controls_parts/Settings.lss b/examples/library/ui_controls_parts/Settings.lss new file mode 100644 index 00000000..cb956a07 --- /dev/null +++ b/examples/library/ui_controls_parts/Settings.lss @@ -0,0 +1,4 @@ +.page { width: 400px; gap: 4px } +input, select { width: fill; height: 30px } +.list { overflow: auto; height: 60px } +p { height: 20px } diff --git a/examples/library/ui_controls_parts/Settings.ludic b/examples/library/ui_controls_parts/Settings.ludic new file mode 100644 index 00000000..63542b85 --- /dev/null +++ b/examples/library/ui_controls_parts/Settings.ludic @@ -0,0 +1,10 @@ +# Settings.ludic - a screen of controls: every change comes back as an event with its value +component Settings { + state music: bool = true + state volume: int = 5 + state quality: int = 1 + state name: string = "Ada" + state jump: int = 32 + state pressed: int = 0 + on go() { pressed += 1 } +} diff --git a/examples/library/ui_controls_parts/Settings.xml b/examples/library/ui_controls_parts/Settings.xml new file mode 100644 index 00000000..61cde68f --- /dev/null +++ b/examples/library/ui_controls_parts/Settings.xml @@ -0,0 +1,12 @@ +
+ + + + + + +

1

2

3

4

5

6

7

8

+

{pressed} {music} {volume} {quality} {name} {jump}

+
diff --git a/examples/library/ui_css.ludic b/examples/library/ui_css.ludic new file mode 100644 index 00000000..90a3feac --- /dev/null +++ b/examples/library/ui_css.ludic @@ -0,0 +1,26 @@ +# ui_css.ludic — ludic.ui's CSS without a renderer: custom properties and var(), absolute, fixed and +# relative positioning, em, rem and vw, @media, text that wraps, sibling combinators, :nth-child +# formulas, :checked, and overflow making a box scroll. +import "ludic.ui" +program UiCss { + numbers float + view Css { + wide = true + } + const LOOK: string = "screen { --gap: 6px; --accent: #ff8800 }\n.panel { position: relative; width: 300px; height: 200px; padding: 10px }\n.badge { position: absolute; right: 5px; bottom: 5px; width: 40px; height: 20px; background: var(--accent) }\n.corner { position: fixed; left: 0; top: 0; width: 10vw; height: 2rem }\n.nudge { position: relative; left: 7px; top: 3px }\n.big { font-size: 2em }\n.missing { color: var(--nope, #00ff00) }\n@media (max-width: 500px) { .wide-only { display: none } }\n@media (min-width: 500px) { .narrow-only { display: none } }\nli + li { margin-top: var(--gap) }\nh3 ~ p { color: var(--accent) }\nli:nth-child(2n+1) { color: #0000ff }\ninput:checked { opacity: 0.25 }\n.list { overflow: auto; height: 50px }\n.para { width: 120px }\n" + const PAGE: string = "\n\n\n
!

moved

big

\n
\n

wide

narrow

\n
  • a
  • b
  • c
\n

h

after

fallback

\n \n

1

2

3

4

\n

a sentence long enough that it has to wrap onto several lines

\n\n\n" + entry (ui_st: mut UiState) { + ui_load_text(ui_st, LOOK, "ui/look.lss") + ui_load_text(ui_st, PAGE, "ui/css.xml") + ui_viewport(ui_st, 400.0, 300.0) + let root: UiNode = ui_nodes(ui_st, "css", view_css()) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 300.0) + let panel = root.children[0] + let badge = panel.children[0] + let ul = root.children[3] + let para = root.children[len(root.children) - 1] + print(`badge at {int(badge.x)},{int(badge.y)} bg {badge.bg}; nudged {int(panel.children[1].x)},{int(panel.children[1].y)}; big {int(panel.children[2].rsize)}; corner {int(root.children[1].x)},{int(root.children[1].y)} {int(root.children[1].cw)}x{int(root.children[1].ch)}`) + print(`media keeps {root.children[2].text}; li gaps {int(ul.children[1].mt)} {int(ul.children[0].mt)}; li colours {ul.children[0].fg} {ul.children[1].fg} {ul.children[2].fg}; after {root.children[5].fg}; fallback {root.children[6].fg}`) + print(`checked opacity {root.children[7].alpha}; list scrolls {root.children[8].kind == UI_SCROLL} {int(root.children[8].ch)} of {int(root.children[8].content_h)}; para lines {len(para.lines)}`) + } +} diff --git a/examples/library/ui_ease.ludic b/examples/library/ui_ease.ludic new file mode 100644 index 00000000..4da174f1 --- /dev/null +++ b/examples/library/ui_ease.ludic @@ -0,0 +1,57 @@ +# ui_ease.ludic — easing never depends on the frame rate: a transition and an animation stepped at +# 60, 120, 180 and 240 Hz on the program's clock are at the same value half way (kept in float, never +# cut to whole pixels) and land exactly on their targets when their time is up. +import "ludic.ui" +program UiEase { + numbers float + state UiEaseState { + dim: bool = false + now: float = 0.0 + } + view Ease (ui_ease_st: UiEaseState) { + dim = ui_ease_st.dim + } + const STYLE: string = "" + const BODY: string = "

a

b

c

" + function clock(ui_ease_st: UiEaseState) -> float { return ui_ease_st.now } + function nodes(ui_st: mut UiState, name: string) -> UiNode { + ui_show(ui_st, name, view_ease(), 0.0, 0.0, 200.0, 200.0) + return ui_nodes(ui_st, name, view_ease()) + } + # one rate, on its own screen: the slide from its first frame, the fade from half a second in + function at_rate(ui_ease_st: mut UiEaseState, ui_st: mut UiState, hz: int) -> string { + let name = `r{hz}` + ui_ease_st.dim = false + var mid = "" + var slid = false + var landed = false + var over = false + for k in 0 .. hz + 1 { + ui_ease_st.now = float(k) / float(hz) + if k == hz / 2 { ui_ease_st.dim = true } + let r = nodes(ui_st, name) + let go = r.children[0] + let soft = r.children[1] + let want = r.children[2].alpha + if k == hz / 10 { mid = `{int(Math.round(go.left * 100.0))} ` } + if k == hz / 2 + hz / 10 { mid = mid + `{int(Math.round(soft.alpha * 1000.0))}` } + if k == hz / 4 { slid = go.left == 37.0 } + if k == hz / 2 + hz / 4 { landed = soft.alpha == want } + if k > hz / 2 + hz / 4 and soft.alpha != want { over = true } + } + return `{hz}: {mid} slid {slid} landed {landed} left {over}` + } + entry (ui_ease_st: mut UiEaseState, ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + ui_clock(ui_st, fn clock) + var page = "" + STYLE + for i in 0 .. 4 { + let hz = 60 * (i + 1) + page = page + `` + BODY + } + ui_load_text(ui_st, page + "", "e.xml") + var out = "" + for i in 0 .. 4 { out = out + at_rate(ui_ease_st, ui_st, 60 * (i + 1)) + "; " } + print(out) + } +} diff --git a/examples/library/ui_emit_click.ludic b/examples/library/ui_emit_click.ludic new file mode 100644 index 00000000..8454d736 --- /dev/null +++ b/examples/library/ui_emit_click.ludic @@ -0,0 +1,40 @@ +# ui_emit_click.ludic - a component whose button says `emit click` is answered by its user's +# on-click, by the mouse or a press: on-click is kept as on-press, and the emit once looked for +# "click" and found nothing (the pack's items and the wardrobe's picks did not respond) +import "ludic.ui" +import "ui_emit_click_parts/Cell.ludic" +import "ui_emit_click_parts/Pick.ludic" +program UiEmitClick { + numbers float + function frame(ui_st: mut UiState, x: float, y: float, down: bool) -> UiNode { + let i = new UiInput + i.x = x + i.y = y + i.down = down + ui_input(ui_st, i) + ui_show(ui_st, "Pick", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "Pick", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + function first_text(n: UiNode) -> string { + if n.text != "" { return n.text } + for i in 0 .. len(n.children) { let t = first_text(n.children[i]); if t != "" { return t } } + return "" + } + entry (ui_st: mut UiState) { + var root = frame(ui_st, -1.0, -1.0, false) + root = frame(ui_st, -1.0, -1.0, false) + let b = root.children[0].children[2].children[0] + let x = b.x + b.cw / 2.0 + let y = b.y + b.ch / 2.0 + root = frame(ui_st, x, y, false) + root = frame(ui_st, x, y, true) + root = frame(ui_st, x, y, false) + root = frame(ui_st, x, y, false) + let clicked = first_text(root) + ui_press(ui_st, root.children[0].children[1].children[0]) + root = frame(ui_st, x, y, false) + print(`{clicked} | pressed: {first_text(root)}`) + } +} diff --git a/examples/library/ui_emit_click_parts/Cell.lss b/examples/library/ui_emit_click_parts/Cell.lss new file mode 100644 index 00000000..b8c03a5a --- /dev/null +++ b/examples/library/ui_emit_click_parts/Cell.lss @@ -0,0 +1 @@ +.cell { width: 100px; height: 40px } diff --git a/examples/library/ui_emit_click_parts/Cell.ludic b/examples/library/ui_emit_click_parts/Cell.ludic new file mode 100644 index 00000000..0ed005d7 --- /dev/null +++ b/examples/library/ui_emit_click_parts/Cell.ludic @@ -0,0 +1,3 @@ +component Cell { + prop caption: string = "" +} diff --git a/examples/library/ui_emit_click_parts/Cell.xml b/examples/library/ui_emit_click_parts/Cell.xml new file mode 100644 index 00000000..a88076ab --- /dev/null +++ b/examples/library/ui_emit_click_parts/Cell.xml @@ -0,0 +1,3 @@ +
+ +
diff --git a/examples/library/ui_emit_click_parts/Pick.lss b/examples/library/ui_emit_click_parts/Pick.lss new file mode 100644 index 00000000..1de0ef03 --- /dev/null +++ b/examples/library/ui_emit_click_parts/Pick.lss @@ -0,0 +1 @@ +div { width: 400px } diff --git a/examples/library/ui_emit_click_parts/Pick.ludic b/examples/library/ui_emit_click_parts/Pick.ludic new file mode 100644 index 00000000..917bb014 --- /dev/null +++ b/examples/library/ui_emit_click_parts/Pick.ludic @@ -0,0 +1,4 @@ +component Pick { + state got: int = -1 + on pick(i: int) { got = i } +} diff --git a/examples/library/ui_emit_click_parts/Pick.xml b/examples/library/ui_emit_click_parts/Pick.xml new file mode 100644 index 00000000..aeefa2b6 --- /dev/null +++ b/examples/library/ui_emit_click_parts/Pick.xml @@ -0,0 +1,5 @@ +
+

got {got}

+ + +
diff --git a/examples/library/ui_fill.ludic b/examples/library/ui_fill.ludic new file mode 100644 index 00000000..c7214bb1 --- /dev/null +++ b/examples/library/ui_fill.ludic @@ -0,0 +1,41 @@ +# ui_fill.ludic — linear-gradient backgrounds (a direction or an angle, stops placed or spread) drawn +# as whole-pixel bands; object-fit contain and cover for a picture of its own size; aspect-ratio +# making a height from a width, and a native's width from its height. +import "ludic.ui" +program UiFill { + numbers float + state UiFillState { + rects: []string = new []string + pics: []string = new []string + clips: int = 0 + } + const STYLE: string = "" + const BODY: string = "
" + function rec_rect(ui_fill_st: mut UiFillState, x: float, y: float, w: float, h: float, c: int, a: float) -> void { push(ui_fill_st.rects, `{int(x)},{int(y)} {int(w)}x{int(h)} {ui_hex(c)}`) } + function rec_image(ui_fill_st: mut UiFillState, src: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void { push(ui_fill_st.pics, `{int(x)},{int(y)} {int(w)}x{int(h)}`) } + function rec_clip(ui_fill_st: mut UiFillState, x: float, y: float, w: float, h: float) -> void { ui_fill_st.clips += 1 } + function no_clip() -> void { } + function no_text(x: float, y: float, s: string, size: float, c: int, a: float) -> void { } + function nat_w(src: string) -> float { return 200.0 } + function nat_h(src: string) -> float { return 100.0 } + function knob_measure(n: UiNode, avail: float) -> void { } + function knob_draw(n: UiNode) -> void { } + entry (ui_fill_st: UiFillState, ui_st: mut UiState) { + let b = new UiBackend + b.rect = fn rec_rect + b.text = fn no_text + b.image = fn rec_image + b.image_w = fn nat_w + b.image_h = fn nat_h + b.clip = fn rec_clip + b.unclip = fn no_clip + ui_backend(ui_st, b) + ui_native(ui_st, "knob", fn knob_measure, fn knob_draw) + ui_load_text(ui_st, "" + STYLE + BODY + "", "f.xml") + ui_show(ui_st, "f", null, 0.0, 0.0, 400.0, 600.0) + let r: UiNode = ui_nodes(ui_st, "f", null) + ui_place(ui_st, r, 0.0, 0.0, 400.0, 600.0) + let n = len(ui_fill_st.rects) + print(`{ui_fill_st.rects[0]} .. {ui_fill_st.rects[49]} | {ui_fill_st.rects[50]} .. {ui_fill_st.rects[59]} .. {ui_fill_st.rects[69]} of {n} | {ui_fill_st.pics[0]} | {ui_fill_st.pics[1]} clipped {ui_fill_st.clips} | ratio {int(r.children[4].ch)} knob {int(r.children[5].children[0].cw)}`) + } +} diff --git a/examples/library/ui_first_show.ludic b/examples/library/ui_first_show.ludic new file mode 100644 index 00000000..c3aeb648 --- /dev/null +++ b/examples/library/ui_first_show.ludic @@ -0,0 +1,27 @@ +# ui_first_show.ludic - a component shown for the first time once the fence is judging (a prompt's key +# hint mid-walk) makes its instance's props and model: declared, once per instance, and the failing +# fence lets it through; hidden and shown again, the instance is reused and nothing more is made: +# bad 0 shown 3 +import "ludic.ui" +import "ui_first_show_parts/Cell.ludic" +import "ui_first_show_parts/Host.ludic" +program UiFirstShow { + numbers float + function frame(ui_st: mut UiState) -> UiNode { + let root: UiNode = ui_nodes(ui_st, "Host", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + entry (ui_st: mut UiState) { + var shown = 0 + for f in 0 .. 1400 { + let root = frame(ui_st) + if f == 700 or f == 900 or f == 1000 or f == 1100 or f == 1200 { + ui_press(ui_st, root.children[0].children[0]) + if f != 900 and f != 1100 { shown += 1 } + } + Mem.frame() + } + print(`bad {Mem.bad_frames()} shown {shown}`) + } +} diff --git a/examples/library/ui_first_show_parts/Cell.lss b/examples/library/ui_first_show_parts/Cell.lss new file mode 100644 index 00000000..b8c03a5a --- /dev/null +++ b/examples/library/ui_first_show_parts/Cell.lss @@ -0,0 +1 @@ +.cell { width: 100px; height: 40px } diff --git a/examples/library/ui_first_show_parts/Cell.ludic b/examples/library/ui_first_show_parts/Cell.ludic new file mode 100644 index 00000000..0ed005d7 --- /dev/null +++ b/examples/library/ui_first_show_parts/Cell.ludic @@ -0,0 +1,3 @@ +component Cell { + prop caption: string = "" +} diff --git a/examples/library/ui_first_show_parts/Cell.xml b/examples/library/ui_first_show_parts/Cell.xml new file mode 100644 index 00000000..a88076ab --- /dev/null +++ b/examples/library/ui_first_show_parts/Cell.xml @@ -0,0 +1,3 @@ +
+ +
diff --git a/examples/library/ui_first_show_parts/Host.lss b/examples/library/ui_first_show_parts/Host.lss new file mode 100644 index 00000000..1bac0ccf --- /dev/null +++ b/examples/library/ui_first_show_parts/Host.lss @@ -0,0 +1 @@ +.t { width: 60px; height: 30px } diff --git a/examples/library/ui_first_show_parts/Host.ludic b/examples/library/ui_first_show_parts/Host.ludic new file mode 100644 index 00000000..a8e1b5ba --- /dev/null +++ b/examples/library/ui_first_show_parts/Host.ludic @@ -0,0 +1,4 @@ +component Host { + state on: bool = false + on flip() { on = not on } +} diff --git a/examples/library/ui_first_show_parts/Host.xml b/examples/library/ui_first_show_parts/Host.xml new file mode 100644 index 00000000..29140558 --- /dev/null +++ b/examples/library/ui_first_show_parts/Host.xml @@ -0,0 +1,4 @@ +
+ + +
diff --git a/examples/library/ui_flex.ludic b/examples/library/ui_flex.ludic new file mode 100644 index 00000000..50255f51 --- /dev/null +++ b/examples/library/ui_flex.ludic @@ -0,0 +1,25 @@ +# ui_flex.ludic — ludic.ui's flex layout and stylesheets without a renderer: grow shares the spare +# room, justify spreads it, a row wraps, align stretches; a stylesheet file (which @imports another) +# styles whoever imports it, a more specific rule wins, a bound class switches one on, and an attribute beats all. +import "ludic.ui" +program UiFlex { + numbers float + let picked: int = 1 + view Tags { + picked = picked + } + const BASE: string = "/* base.lss - every text small */\ntext { size: 10 }\n" + const THEME: string = "/* theme.lss - the base, and a colour for what is on */\n@import \"base.lss\";\n.on { color: #ff0000 }\n" + const PAGE: string = "\n\n\n\n \n ab\n \n lmr\n \n t{i}\n \n
x\n\n\n" + entry (ui_st: mut UiState) { + ui_load_text(ui_st, BASE, "ui/base.lss") + ui_load_text(ui_st, THEME, "ui/theme.lss") + ui_load_text(ui_st, PAGE, "ui/flex.xml") + let root: UiNode = ui_nodes(ui_st, "flex", view_tags()) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 300.0) + let bar = root.children[0] + let wrap = root.children[2] + let tall = root.children[3] + print(`grow {int(bar.children[0].cw)}:{int(bar.children[1].cw)}; between at {int(root.children[1].children[1].x)}; wrap lines at {int(wrap.children[0].y)}, {int(wrap.children[3].y)}; red {wrap.children[1].fg}; stretched {int(tall.children[0].ch)}; own size {int(tall.children[1].rsize)}`) + } +} diff --git a/examples/library/ui_focus_tip.ludic b/examples/library/ui_focus_tip.ludic new file mode 100644 index 00000000..126839ce --- /dev/null +++ b/examples/library/ui_focus_tip.ludic @@ -0,0 +1,41 @@ +# ui_focus_tip.ludic — a title shows for the keyboard's focus as it does for the pointer: after the +# focus has rested half a second on an element with one, under that element; moving the pointer +# hands the tooltip back to the pointer. +import "ludic.ui" +program UiFocusTip { + numbers float + state UiFocusTipState { + said: []string = new []string + now: float = 0.0 + } + const PAGE: string = "" + function rec_text(ui_focus_tip_st: mut UiFocusTipState, x: float, y: float, s: string, size: float, c: int, a: float) -> void { push(ui_focus_tip_st.said, `{s} {int(x)},{int(y)}`) } + function clock(ui_focus_tip_st: UiFocusTipState) -> float { return ui_focus_tip_st.now } + function frame(ui_focus_tip_st: mut UiFocusTipState, ui_st: mut UiState, i: UiInput, secs: float) -> string { + ui_focus_tip_st.now = ui_focus_tip_st.now + secs + ui_focus_tip_st.said = new []string + ui_input(ui_st, i) + ui_show(ui_st, "f", null, 0.0, 0.0, 400.0, 400.0) + return ui_focus_tip_st.said[len(ui_focus_tip_st.said) - 1] + } + entry (ui_focus_tip_st: mut UiFocusTipState, ui_st: mut UiState) { + let b = new UiBackend + b.text = fn rec_text + ui_backend(ui_st, b) + ui_clock(ui_st, fn clock) + ui_load_text(ui_st, PAGE, "f.xml") + frame(ui_focus_tip_st, ui_st, new UiInput, 0.0) + let tab = new UiInput + tab.tab = true + frame(ui_focus_tip_st, ui_st, tab, 0.0) + let early = frame(ui_focus_tip_st, ui_st, new UiInput, 0.2) + let shown = frame(ui_focus_tip_st, ui_st, new UiInput, 0.4) + frame(ui_focus_tip_st, ui_st, tab, 0.0) + let next = frame(ui_focus_tip_st, ui_st, new UiInput, 0.6) + let moved = new UiInput + moved.x = 390.0 + moved.y = 390.0 + let gone = frame(ui_focus_tip_st, ui_st, moved, 0.6) + print(`{early} | {shown} | {next} | {gone}`) + } +} diff --git a/examples/library/ui_gamepad.ludic b/examples/library/ui_gamepad.ludic new file mode 100644 index 00000000..b6c257a4 --- /dev/null +++ b/examples/library/ui_gamepad.ludic @@ -0,0 +1,54 @@ +# ui_gamepad.ludic — the pad and held keys, on the program's clock: a direction held presses once, +# again after 0.42 s and every 0.11 s after that, at any frame rate; A presses what has the focus, +# left and right step a range and a select, and B closes a popover. +import "ludic.ui" +import "ui_gamepad_parts/Pad.ludic" +program UiGamepad { + numbers float + state UiGamepadState { + now: float = 0.0 + } + function clock(ui_gamepad_st: UiGamepadState) -> float { return ui_gamepad_st.now } + function frame(ui_st: mut UiState, i: UiInput) -> string { + ui_input(ui_st, i) + ui_show(ui_st, "Pad", null, 0.0, 0.0, 400.0, 600.0) + let page = ui_nodes(ui_st, "Pad", null).children[0] + return page.children[len(page.children) - 1].text + } + # `what` held for `secs` at `hz`, then let go + function hold(ui_gamepad_st: mut UiGamepadState, ui_st: mut UiState, what: string, secs: float, hz: int) -> string { + let frames = int(secs * float(hz) + 0.5) + for k in 0 .. frames + 1 { + ui_gamepad_st.now = ui_gamepad_st.now + 1.0 / float(hz) + let i = new UiInput + i.held_up = what == "up" + i.held_down = what == "down" + i.held_left = what == "left" + i.held_right = what == "right" + i.pad_a = what == "a" + i.pad_b = what == "b" + frame(ui_st, i) + } + ui_gamepad_st.now = ui_gamepad_st.now + 1.0 / float(hz) + return frame(ui_st, new UiInput) + } + function tap(ui_gamepad_st: mut UiGamepadState, ui_st: mut UiState, what: string) -> string { return hold(ui_gamepad_st, ui_st, what, 0.0, 60) } + entry (ui_gamepad_st: mut UiGamepadState, ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + ui_clock(ui_st, fn clock) + frame(ui_st, new UiInput) + hold(ui_gamepad_st, ui_st, "down", 1.0, 240) + let fast = tap(ui_gamepad_st, ui_st, "a") + hold(ui_gamepad_st, ui_st, "down", 1.0, 60) + hold(ui_gamepad_st, ui_st, "up", 0.5, 60) + let slow = tap(ui_gamepad_st, ui_st, "a") + for k in 0 .. 8 { tap(ui_gamepad_st, ui_st, "down") } + let vol = hold(ui_gamepad_st, ui_st, "right", 0.5, 60) + tap(ui_gamepad_st, ui_st, "down") + let q = tap(ui_gamepad_st, ui_st, "left") + tap(ui_gamepad_st, ui_st, "down") + let open = tap(ui_gamepad_st, ui_st, "a") + let shut = tap(ui_gamepad_st, ui_st, "b") + print(`{fast} | {slow} | {vol} | {q} | {open} | {shut}`) + } +} diff --git a/examples/library/ui_gamepad_parts/Pad.ludic b/examples/library/ui_gamepad_parts/Pad.ludic new file mode 100644 index 00000000..6d3776cc --- /dev/null +++ b/examples/library/ui_gamepad_parts/Pad.ludic @@ -0,0 +1,7 @@ +# Pad.ludic - eight buttons, a range, a select and a button that opens a popover, for a gamepad +component Pad { + state last: int = -1 + state vol: int = 0 + state q: int = 0 + state open: bool = false +} diff --git a/examples/library/ui_gamepad_parts/Pad.xml b/examples/library/ui_gamepad_parts/Pad.xml new file mode 100644 index 00000000..0596d59f --- /dev/null +++ b/examples/library/ui_gamepad_parts/Pad.xml @@ -0,0 +1,8 @@ +
+ + + + +
+

last {last} vol {vol} q {q} open {open}

+
diff --git a/examples/library/ui_hold.ludic b/examples/library/ui_hold.ludic new file mode 100644 index 00000000..60547577 --- /dev/null +++ b/examples/library/ui_hold.ludic @@ -0,0 +1,61 @@ +# ui_hold.ludic — on-hold fires every frame a button is held, by the pointer (still when it is dragged +# off, until let go) or by Enter or the pad's A on the focus, with event.dt and event.t on the ui clock, +# the same at 60 and 240 Hz; on-down and on-up still mark the ends. +import "ludic.ui" +program UiHold { + numbers float + state UiHoldState { + now: float = 0.0 + held: float = 0.0 + frames: int = 0 + last_t: float = 0.0 + ends: string = "" + } + view Craft (ui_hold_st: mut UiHoldState) { + on tick(dt: float, t: float) { + ui_hold_st.held = ui_hold_st.held + dt + ui_hold_st.frames = ui_hold_st.frames + 1 + ui_hold_st.last_t = t + } + on down() { ui_hold_st.ends = ui_hold_st.ends + "d" } + on up() { ui_hold_st.ends = ui_hold_st.ends + "u" } + } + const PAGE: string = "" + function clock(ui_hold_st: UiHoldState) -> float { return ui_hold_st.now } + function frame(ui_st: mut UiState, i: UiInput) -> UiNode { + ui_input(ui_st, i) + ui_show(ui_st, "h", view_craft(), 0.0, 0.0, 400.0, 400.0) + let r: UiNode = ui_nodes(ui_st, "h", view_craft()) + ui_place(ui_st, r, 0.0, 0.0, 400.0, 400.0) + return r.children[0] + } + # held for `secs` at `hz`, by the pointer (off the button after the first frame) or by A + function hold(ui_hold_st: mut UiHoldState, ui_st: mut UiState, b: UiNode, secs: float, hz: int, pad: bool) -> string { + ui_hold_st.held = 0.0 + ui_hold_st.frames = 0 + let n = int(secs * float(hz) + 0.5) + for k in 0 .. n + 1 { + let i = new UiInput + i.pad_a = pad + i.down = not pad + i.x = b.x + 5.0 + i.y = b.y + 5.0 + if k > 0 and not pad { i.x = 390.0 } + frame(ui_st, i) + ui_hold_st.now = ui_hold_st.now + 1.0 / float(hz) + } + frame(ui_st, new UiInput) + ui_hold_st.now = ui_hold_st.now + 1.0 / float(hz) + return `{ui_hold_st.frames} frames {int(Math.round(ui_hold_st.held * 1000.0))} ms t {int(Math.round(ui_hold_st.last_t * 1000.0))}` + } + entry (ui_hold_st: mut UiHoldState, ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + ui_clock(ui_st, fn clock) + ui_load_text(ui_st, PAGE, "h.xml") + let b = frame(ui_st, new UiInput) + let slow = hold(ui_hold_st, ui_st, b, 0.5, 60, false) + let fast = hold(ui_hold_st, ui_st, b, 0.5, 240, false) + let pad = hold(ui_hold_st, ui_st, b, 0.25, 60, true) + print(`{slow} | {fast} | pad {pad} | {ui_hold_st.ends}`) + } +} diff --git a/examples/library/ui_html.ludic b/examples/library/ui_html.ludic new file mode 100644 index 00000000..f639d5bc --- /dev/null +++ b/examples/library/ui_html.ludic @@ -0,0 +1,30 @@ +# ui_html.ludic — ludic.ui read as HTML and CSS: HTML's elements with a browser's defaults, ids, +# classes, style="...", hidden and disabled, onclick; selectors with ids, descendants, children, +# attributes and pseudo-classes, weighed by specificity; CSS's own property names and the box model. +import "ludic.ui" +program UiHtml { + numbers float + state UiHtmlState { + clicks: int = 0 + } + view Page (ui_html_st: mut UiHtmlState) { + clicks = ui_html_st.clicks + on clicked() { ui_html_st.clicks += 1 } + } + const LOOK: string = "/* look.lss */\n.card { padding: 4 8 12 16; margin: 3; border: 2px solid #0000ff; width: 50% }\n#title { font-size: 40 }\nh1.big { font-size: 20 }\n.card p { color: #00ff00 }\nul > li { height: 30 }\nli:first-child { color: #ff0000 }\nli:nth-child(even) { color: #0000ff }\nli:last-child:not(.keep) { display: none }\n[kind=warn] { background-color: #ffff00 }\nbutton:disabled { opacity: 0.5 }\n" + const PAGE: string = "\n\n\n

Title

\n

inside

careful
\n

outside

\n
  • one
  • two
  • three
  • gone
\n
\n

mid

\n \n \n
\n
\n" + entry (ui_html_st: UiHtmlState, ui_st: mut UiState) { + ui_load_text(ui_st, LOOK, "ui/look.lss") + ui_load_text(ui_st, PAGE, "ui/page.ludic.xml") + var root: UiNode = ui_nodes(ui_st, "page", view_page()) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 600.0) + let card = root.children[1] + let items = root.children[3].children + print(`id beats class: {int(root.children[0].rsize)}; card {int(card.cw)} wide at {int(card.x)},{int(card.y)} pad {int(card.pt)} {int(card.pr)} {int(card.pb)} {int(card.pl)} border {int(card.border)}`) + print(`inside {card.children[0].fg}, outside {root.children[2].fg}; warn bg {card.children[1].bg}; items {len(items)} of 30: {int(items[0].ch)}; colours {items[0].fg} {items[1].fg} {items[2].fg}; hr {int(root.children[4].ch)}; children {len(root.children)}`) + ui_press(ui_st, root.children[6]) + root = ui_nodes(ui_st, "page", view_page()) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 600.0) + print(`clicks {ui_html_st.clicks}; the button now disabled {not root.children[6].enabled} at opacity {root.children[6].alpha}; text-align {root.children[5].talign}`) + } +} diff --git a/examples/library/ui_inputs.ludic b/examples/library/ui_inputs.ludic new file mode 100644 index 00000000..4a995afc --- /dev/null +++ b/examples/library/ui_inputs.ludic @@ -0,0 +1,83 @@ +# ui_inputs.ludic — more of ludic.ui's controls, driven by the keys: a number field takes typed digits +# (Enter or leaving it commits them, held between min and max) and steps with the arrows; a key field +# listens for the next key (Tab included), Esc stops it listening and Backspace unbinds it; a range +# shows its value as a percentage or with decimals and a unit; a label carries a note; an action may +# call an event named set; and a string prop given a number reads it as text. +import "ludic.ui" +import "ui_inputs_parts/Form.ludic" +import "ui_inputs_parts/Tag.ludic" +program UiInputs { + numbers float + function frame(ui_st: mut UiState, i: UiInput) -> UiNode { + ui_input(ui_st, i) + ui_show(ui_st, "Form", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "Form", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root.children[0] + } + function keys(tab: bool, shift: bool, enter: bool, left: bool) -> UiInput { + let i = new UiInput + i.tab = tab + i.shift = shift + i.enter = enter + i.left = left + if tab { i.key = Key.Tab } + if enter { i.key = Key.Enter } + return i + } + function typed(s: string) -> UiInput { + let i = new UiInput + i.typed = s + return i + } + function shown_state(page: UiNode) -> string { return page.children[len(page.children) - 1].text } + function last(n: UiNode) -> string { return n.children[len(n.children) - 1].text } + entry (ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + var page = frame(ui_st, new UiInput) + frame(ui_st, keys(true, false, false, false)) + frame(ui_st, typed("12")) + page = frame(ui_st, keys(false, false, true, false)) + let shown = page.children[0].children[1].children[0].text + let a = shown_state(page) + frame(ui_st, typed("99")) + page = frame(ui_st, keys(true, false, false, false)) + let b = shown_state(page) + frame(ui_st, keys(true, true, false, false)) + page = frame(ui_st, keys(false, false, false, true)) + let c = shown_state(page) + let note = page.children[0].children[0].children[1].text + frame(ui_st, keys(true, false, false, false)) + let space = page.children[1].children[1].children[0].text + frame(ui_st, keys(false, false, true, false)) + let cap = ui_capturing(ui_st) + page = frame(ui_st, keys(true, false, false, false)) + let d = shown_state(page) + frame(ui_st, keys(false, false, true, false)) + let esc = new UiInput + esc.escape = true + esc.key = Key.Escape + page = frame(ui_st, esc) + let e = shown_state(page) + let back = new UiInput + back.backspace = true + page = frame(ui_st, back) + let f = shown_state(page) + frame(ui_st, keys(true, false, false, false)) + let right = new UiInput + right.right = true + page = frame(ui_st, right) + page = frame(ui_st, new UiInput) + let button = page.children[4] + let click = new UiInput + click.x = button.x + 2.0 + click.y = button.y + 2.0 + click.down = true + frame(ui_st, click) + let up = new UiInput + up.x = click.x + up.y = click.y + page = frame(ui_st, up) + print(`count {shown} {a} | {b} | {c} | {note} | {space} listening {cap} {d} | {e} | {f} | {last(page.children[2])} {last(page.children[3])} | {shown_state(page)} | {page.children[5].text}`) + } +} diff --git a/examples/library/ui_inputs_parts/Form.lss b/examples/library/ui_inputs_parts/Form.lss new file mode 100644 index 00000000..0f774ffd --- /dev/null +++ b/examples/library/ui_inputs_parts/Form.lss @@ -0,0 +1,2 @@ +.page { width: 400px; gap: 4px } +input { width: fill } diff --git a/examples/library/ui_inputs_parts/Form.ludic b/examples/library/ui_inputs_parts/Form.ludic new file mode 100644 index 00000000..f81669f3 --- /dev/null +++ b/examples/library/ui_inputs_parts/Form.ludic @@ -0,0 +1,9 @@ +# Form.ludic - a number, a key binding, two ranges with their values formatted, a note under a label, +# and an event called set +component Form { + state count: int = 5 + state bind: int = 32 + state vol: float = 0.5 + state said: int = 0 + on set(v: int) { said = v } +} diff --git a/examples/library/ui_inputs_parts/Form.xml b/examples/library/ui_inputs_parts/Form.xml new file mode 100644 index 00000000..1d13d356 --- /dev/null +++ b/examples/library/ui_inputs_parts/Form.xml @@ -0,0 +1,9 @@ +
+ + + + + + +

{count} {bind} {vol} {said}

+
diff --git a/examples/library/ui_inputs_parts/Tag.ludic b/examples/library/ui_inputs_parts/Tag.ludic new file mode 100644 index 00000000..196cadfa --- /dev/null +++ b/examples/library/ui_inputs_parts/Tag.ludic @@ -0,0 +1,4 @@ +# Tag.ludic - a string prop, which a number given to it arrives in as text +component Tag { + prop text: string = "none" +} diff --git a/examples/library/ui_inputs_parts/Tag.xml b/examples/library/ui_inputs_parts/Tag.xml new file mode 100644 index 00000000..6fd5cee1 --- /dev/null +++ b/examples/library/ui_inputs_parts/Tag.xml @@ -0,0 +1 @@ +

{text}!

diff --git a/examples/library/ui_keycap.ludic b/examples/library/ui_keycap.ludic new file mode 100644 index 00000000..9a3ad764 --- /dev/null +++ b/examples/library/ui_keycap.ludic @@ -0,0 +1,57 @@ +# ui_keycap.ludic — a key field takes a mouse button as well as a key: listening (and :capturing, +# which a stylesheet can colour), the next press of the left, right or middle button anywhere is its +# value (UI_MOUSE_LEFT / RIGHT / MIDDLE), named for what it is, and that press presses nothing else. +import "ludic.ui" +program UiKeycap { + numbers float + state UiKeycapState { + bind: int = 32 + pressed: int = 0 + } + view Keys (ui_keycap_st: mut UiKeycapState) { + bind = ui_keycap_st.bind + pressed = ui_keycap_st.pressed + on set_bind(v: int) { ui_keycap_st.bind = v } + on press() { ui_keycap_st.pressed = ui_keycap_st.pressed + 1 } + } + const PAGE: string = "" + function frame(ui_st: mut UiState, x: float, y: float, b: int) -> UiNode { + let i = new UiInput + i.x = x + i.y = y + i.down = b == 0 + i.down_right = b == 1 + i.down_middle = b == 2 + ui_input(ui_st, i) + ui_show(ui_st, "k", view_keys(), 0.0, 0.0, 400.0, 400.0) + let r: UiNode = ui_nodes(ui_st, "k", view_keys()) + ui_place(ui_st, r, 0.0, 0.0, 400.0, 400.0) + return r + } + function listen(ui_st: mut UiState, k: UiNode) -> int { + frame(ui_st, k.x + 5.0, k.y + 5.0, 0) + frame(ui_st, k.x + 5.0, k.y + 5.0, -1) + return frame(ui_st, -1.0, -1.0, -1).children[0].bg + } + function take(ui_keycap_st: UiKeycapState, ui_st: mut UiState, at: UiNode, b: int) -> string { + frame(ui_st, at.x + 5.0, at.y + 5.0, b) + frame(ui_st, at.x + 5.0, at.y + 5.0, -1) + let r = frame(ui_st, -1.0, -1.0, -1) + let f = r.children[0] + return `{ui_keycap_st.bind} {f.children[1].children[0].text} {f.bg}` + } + entry (ui_keycap_st: UiKeycapState, ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + ui_load_text(ui_st, PAGE, "k.xml") + let r = frame(ui_st, -1.0, -1.0, -1) + let k = r.children[0] + let btn = r.children[1] + let red = listen(ui_st, k) + let right = take(ui_keycap_st, ui_st, btn, 1) + listen(ui_st, k) + let middle = take(ui_keycap_st, ui_st, btn, 2) + listen(ui_st, k) + let left = take(ui_keycap_st, ui_st, btn, 0) + print(`listening {red} | {right} | {middle} | {left} | pressed {ui_keycap_st.pressed}`) + } +} diff --git a/examples/library/ui_look.ludic b/examples/library/ui_look.ludic new file mode 100644 index 00000000..0e279b45 --- /dev/null +++ b/examples/library/ui_look.ludic @@ -0,0 +1,67 @@ +# ui_look.ludic — what ludic.ui draws, through a backend that writes each call down: a component +# whose root is another component, styled by every sheet above it by specificity; text-shadow under +# text; a native reading the opacity it is drawn at (ui_opacity); border-image as painted, tinted by +# a background colour, and gone under `transparent`; a picture drawn as it is and an atlas cell in +# the text's colour; a tooltip of two lines; and popovers placed beside their anchors, one flipped. +import "ludic.ui" +import "ui_look_parts/Look.ludic" +import "ui_look_parts/Middle.ludic" +import "ui_look_parts/Inner.ludic" +import "ui_look_parts/Menu.ludic" +program UiLook { + numbers float + state UiLookState { + said: []string = new []string + seen: float = 0.0 + } + function rec_rect(x: float, y: float, w: float, h: float, c: int, a: float) -> void { } + function rec_text(ui_look_st: mut UiLookState, x: float, y: float, s: string, size: float, c: int, a: float) -> void { push(ui_look_st.said, `text {s} {int(x)},{int(y)} {ui_hex(c)}`) } + function rec_image(ui_look_st: mut UiLookState, src: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void { push(ui_look_st.said, `image {src} {ui_hex(c)}`) } + function rec_nine(ui_look_st: mut UiLookState, src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void { push(ui_look_st.said, `nine {int(w)} {ui_hex(c)}`) } + function gauge_measure(n: UiNode, avail: float) -> void { } + function gauge_draw(ui_look_st: mut UiLookState, ui_st: UiState, n: UiNode) -> void { ui_look_st.seen = ui_opacity(ui_st) } + function count(ui_look_st: UiLookState, s: string) -> int { + var n = 0 + for i in 0 .. len(ui_look_st.said) { + if Text.starts_with(ui_look_st.said[i], s) { n += 1 } + } + return n + } + function has(ui_look_st: UiLookState, s: string) -> string { + for i in 0 .. len(ui_look_st.said) { + if Text.starts_with(ui_look_st.said[i], s) { return ui_look_st.said[i] } + } + return "-" + } + function show(ui_look_st: mut UiLookState, ui_st: mut UiState, name: string, i: UiInput) -> UiNode { + ui_input(ui_st, i) + ui_look_st.said = new []string + ui_show(ui_st, name, null, 0.0, 0.0, 400.0, 300.0) + let root: UiNode = ui_nodes(ui_st, name, null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 300.0) + return root.children[0] + } + entry (ui_look_st: mut UiLookState, ui_st: mut UiState) { + let b = new UiBackend + b.rect = fn rec_rect + b.text = fn rec_text + b.image = fn rec_image + b.nine = fn rec_nine + ui_backend(ui_st, b) + ui_native(ui_st, "gauge", fn gauge_measure, fn gauge_draw) + var page = show(ui_look_st, ui_st, "Look", new UiInput) + let inner = page.children[0] + let tip = page.children[7] + let rest = new UiInput + rest.x = tip.x + 2.0 + rest.y = tip.y + 2.0 + for f in 0 .. 40 { page = show(ui_look_st, ui_st, "Look", rest) } + print(`root {inner.tag}.{inner.classes[0]}.{inner.classes[1]}.{inner.classes[2]} pad {int(inner.pt)} margin {int(inner.mt)} color {ui_hex(inner.fg)}`) + print(`{has(ui_look_st, "text Hi")} | {has(ui_look_st, "text Hi 0")} | opacity {ui_look_st.seen} | {has(ui_look_st, "nine 40 #ffffff")} {has(ui_look_st, "nine 40 #ff0000")} of {count(ui_look_st, "nine")}`) + print(`{has(ui_look_st, "image photo")} | {has(ui_look_st, "image icon")} | {has(ui_look_st, "text first")} | {has(ui_look_st, "text second")}`) + let menu = show(ui_look_st, ui_st, "Menu", new UiInput) + let a = menu.children[1] + let c = menu.children[3] + print(`by id at {int(a.x)},{int(a.y)} | flipped at {int(c.x)},{int(c.y)} {int(c.cw)}x{int(c.ch)}`) + } +} diff --git a/examples/library/ui_look_parts/Inner.lss b/examples/library/ui_look_parts/Inner.lss new file mode 100644 index 00000000..a2a23a45 --- /dev/null +++ b/examples/library/ui_look_parts/Inner.lss @@ -0,0 +1 @@ +div.in { padding: 5px } diff --git a/examples/library/ui_look_parts/Inner.ludic b/examples/library/ui_look_parts/Inner.ludic new file mode 100644 index 00000000..ea13f836 --- /dev/null +++ b/examples/library/ui_look_parts/Inner.ludic @@ -0,0 +1,4 @@ +# Inner.ludic - the innermost root: its own rule is more specific than Middle's, so it holds +component Inner { + state n: int = 0 +} diff --git a/examples/library/ui_look_parts/Inner.xml b/examples/library/ui_look_parts/Inner.xml new file mode 100644 index 00000000..6ac2251d --- /dev/null +++ b/examples/library/ui_look_parts/Inner.xml @@ -0,0 +1 @@ +

in

diff --git a/examples/library/ui_look_parts/Look.lss b/examples/library/ui_look_parts/Look.lss new file mode 100644 index 00000000..10bfe048 --- /dev/null +++ b/examples/library/ui_look_parts/Look.lss @@ -0,0 +1,9 @@ +.page { width: 400px; gap: 2px } +.m { margin: 9px } +.shade { text-shadow: 2px 3px 4px #102030 } +.dim { opacity: 0.5 } +gauge { width: 10px; height: 10px } +.chip { width: 40px; height: 20px; border-image: url(chip.png) 8 } +.clear { background: transparent } +.red { background: #ff0000 } +.pics { flex-direction: row; color: #00ff00 } diff --git a/examples/library/ui_look_parts/Look.ludic b/examples/library/ui_look_parts/Look.ludic new file mode 100644 index 00000000..6dcb508d --- /dev/null +++ b/examples/library/ui_look_parts/Look.ludic @@ -0,0 +1,5 @@ +# Look.ludic - what is drawn: a nested component's root styled by every sheet above it, a shadow +# under text, a native at its group's opacity, border-images, pictures, and a tooltip of two lines +component Look { + state n: int = 0 +} diff --git a/examples/library/ui_look_parts/Look.xml b/examples/library/ui_look_parts/Look.xml new file mode 100644 index 00000000..953c4731 --- /dev/null +++ b/examples/library/ui_look_parts/Look.xml @@ -0,0 +1,10 @@ +
+ +

Hi

+
+
+
+
+
+

rest here

+
diff --git a/examples/library/ui_look_parts/Menu.lss b/examples/library/ui_look_parts/Menu.lss new file mode 100644 index 00000000..c391c723 --- /dev/null +++ b/examples/library/ui_look_parts/Menu.lss @@ -0,0 +1,4 @@ +.page { width: 400px; height: 300px } +.cell { position: absolute; left: 20px; top: 50px; width: 60px; height: 30px } +.edge { position: absolute; left: 350px; top: 280px; width: 50px; height: 30px } +.verbs { width: 100px; margin-left: 4px; margin-right: 4px } diff --git a/examples/library/ui_look_parts/Menu.ludic b/examples/library/ui_look_parts/Menu.ludic new file mode 100644 index 00000000..040af0e7 --- /dev/null +++ b/examples/library/ui_look_parts/Menu.ludic @@ -0,0 +1,5 @@ +# Menu.ludic - popovers beside what they belong to: one by the anchor's id, one beside the element +# before it, which has no room on its right and opens to the left +component Menu { + state n: int = 0 +} diff --git a/examples/library/ui_look_parts/Menu.xml b/examples/library/ui_look_parts/Menu.xml new file mode 100644 index 00000000..f1d58856 --- /dev/null +++ b/examples/library/ui_look_parts/Menu.xml @@ -0,0 +1,6 @@ +
+ +
+ +
+
diff --git a/examples/library/ui_look_parts/Middle.lss b/examples/library/ui_look_parts/Middle.lss new file mode 100644 index 00000000..25c4e0e7 --- /dev/null +++ b/examples/library/ui_look_parts/Middle.lss @@ -0,0 +1 @@ +.i { padding: 3px; color: #ff0000 } diff --git a/examples/library/ui_look_parts/Middle.ludic b/examples/library/ui_look_parts/Middle.ludic new file mode 100644 index 00000000..f849c893 --- /dev/null +++ b/examples/library/ui_look_parts/Middle.ludic @@ -0,0 +1,4 @@ +# Middle.ludic - a component whose root is another component +component Middle { + state n: int = 0 +} diff --git a/examples/library/ui_look_parts/Middle.xml b/examples/library/ui_look_parts/Middle.xml new file mode 100644 index 00000000..eef6d672 --- /dev/null +++ b/examples/library/ui_look_parts/Middle.xml @@ -0,0 +1 @@ + diff --git a/examples/library/ui_mounted.ludic b/examples/library/ui_mounted.ludic new file mode 100644 index 00000000..083468b4 --- /dev/null +++ b/examples/library/ui_mounted.ludic @@ -0,0 +1,18 @@ +# ui_mounted.ludic - an editor's questions of a running interface: which components the last frame +# showed (in tree order, each once) and one's model, read in place. App shows two Counters: the list +# is App then Counter, a Counter has a model, and a class not shown has none. Prints `App|Counter| 1 1`. +import "ludic.ui" +import "ui_component_parts/Counter.ludic" +import "ui_component_parts/App.ludic" +program UiMounted { + numbers float + entry (ui_st: mut UiState) { + let root: UiNode = ui_nodes(ui_st, "App", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + let names = new []string + ui_mounted(ui_st, names) + var joined = "" + for i in 0 .. len(names) { joined = joined + names[i] + "|" } + print(`{joined} {ui_model_of(ui_st, "Counter") != null} {ui_model_of(ui_st, "Nothing") == null}`) + } +} diff --git a/examples/library/ui_nine.ludic b/examples/library/ui_nine.ludic new file mode 100644 index 00000000..ca2efdbd --- /dev/null +++ b/examples/library/ui_nine.ludic @@ -0,0 +1,19 @@ +# ui_nine.ludic — a nine-slice's cuts: its corners are at most half the box across and half of it +# down, each direction on its own, so a key cap shorter than two corners keeps them across; the cuts +# land on whole pixels so the pieces meet. +import "ludic.ui" +program UiNine { + numbers float + function cuts(x: float, y: float, w: float, h: float, dst: float) -> string { + let c = ui_nine_cuts(x, y, w, h, dst) + var s = "" + for i in 0 .. 8 { + if i == 4 { s = s + "/" } + s = s + ` {int(c[i])}` + } + return s + } + entry { + print(`cap{cuts(0.0, 0.0, 30.0, 16.0, 12.0)} | wide{cuts(0.0, 0.0, 100.0, 40.0, 12.0)} | odd{cuts(10.4, 3.6, 20.3, 9.0, 14.0)}`) + } +} diff --git a/examples/library/ui_override.ludic b/examples/library/ui_override.ludic new file mode 100644 index 00000000..867341e2 --- /dev/null +++ b/examples/library/ui_override.ludic @@ -0,0 +1,36 @@ +# ui_override.ludic - a template's text given from outside in place of its file (a studio editing a +# running game's interface): the component is shown again with the new template and its state kept, +# and letting go brings the file back, the state still kept. Prints three lines: the counters as the +# files have them, as the override has them, and as the files have them again. +import "ludic.ui" +import "ui_component_parts/Counter.ludic" +import "ui_component_parts/App.ludic" +program UiOverride { + numbers float + function texts(n: UiNode, out: []string) -> void { + if n.text != "" { push(out, n.text) } + for i in 0 .. len(n.children) { texts(n.children[i], out) } + } + function frame(ui_st: mut UiState) -> UiNode { + let root: UiNode = ui_nodes(ui_st, "App", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + function said(ui_st: mut UiState) -> string { + let out = new []string + texts(frame(ui_st), out) + var joined = "" + for i in 0 .. len(out) { joined = joined + out[i] + "|" } + return joined + } + entry (ui_st: mut UiState) { + let root = frame(ui_st) + ui_press(ui_st, root.children[0].children[1].children[1]) # counter a: + once + print(said(ui_st)) + let xml = "examples/library/ui_component_parts/Counter.xml" + ui_override(ui_st, xml, "

[{label}={count}]

") + print(`{said(ui_st)} {ui_overridden(ui_st, xml)}`) + ui_override_clear(ui_st, "") + print(`{said(ui_st)} {ui_overridden(ui_st, xml)}`) + } +} diff --git a/examples/library/ui_override_up.ludic b/examples/library/ui_override_up.ludic new file mode 100644 index 00000000..0a5c5824 --- /dev/null +++ b/examples/library/ui_override_up.ludic @@ -0,0 +1,26 @@ +# ui_override_up.ludic - the override through a component registered under a path with `..` in it, as a +# game's lab build registers every component (lab/../src/ui/...): the studio names the file as the game's +# root has it, and it still matches. Prints `counters|[a=0]|+|[b=0]|+|outside| 1 1` - the override shown, +# and the same file found by its plain path and by a ./ path. +import "ludic.ui" +import "../library/ui_component_parts/Counter.ludic" +import "../library/ui_component_parts/App.ludic" +program UiOverrideUp { + numbers float + function texts(n: UiNode, out: []string) -> void { + if n.text != "" { push(out, n.text) } + for i in 0 .. len(n.children) { texts(n.children[i], out) } + } + entry (ui_st: mut UiState) { + ui_override(ui_st, "examples/library/ui_component_parts/Counter.xml", "

[{label}={count}]

") + let root: UiNode = ui_nodes(ui_st, "App", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + let out = new []string + texts(root, out) + var joined = "" + for i in 0 .. len(out) { joined = joined + out[i] + "|" } + let plain = ui_overridden(ui_st, "examples/library/ui_component_parts/Counter.xml") + let dotted = ui_overridden(ui_st, "./examples/library/../library/ui_component_parts/Counter.xml") + print(`{joined} {plain} {dotted}`) + } +} diff --git a/examples/library/ui_pointer.ludic b/examples/library/ui_pointer.ludic new file mode 100644 index 00000000..3a6e615d --- /dev/null +++ b/examples/library/ui_pointer.ludic @@ -0,0 +1,62 @@ +# ui_pointer.ludic — pointer events: a native map takes presses, drags (still its own when the +# pointer leaves it, until let go), the wheel and the release, but not a press on the button over it; +# a pad takes the same in markup; a hold button says on-down and on-up, and is :active while held. +import "ludic.ui" +import "ui_pointer_parts/Chart.ludic" +program UiPointer { + numbers float + state UiPointerState { + log: string = "" + dragged: float = 0.0 + } + function map_measure(n: UiNode, avail: float) -> void { } + function map_draw(n: UiNode) -> void { } + function map_input(ui_pointer_st: mut UiPointerState, n: UiNode, e: UiPointer) -> bool { + if e.kind == "drag" { ui_pointer_st.dragged = ui_pointer_st.dragged + e.dx } else if e.kind != "pointermove" { ui_pointer_st.log = ui_pointer_st.log + `{e.kind} {int(e.x)},{int(e.y)} {e.button} {int(e.wheel)}; ` } + return true + } + function frame(ui_st: mut UiState, x: float, y: float, down: bool, wheel: float, enter: bool) -> UiNode { + let i = new UiInput + i.x = x + i.y = y + i.down = down + i.wheel = wheel + i.held_enter = enter + ui_input(ui_st, i) + ui_show(ui_st, "Chart", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "Chart", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root.children[0] + } + function said(page: UiNode) -> string { return page.children[len(page.children) - 1].text } + entry (ui_pointer_st: UiPointerState, ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + ui_native(ui_st, "chartmap", fn map_measure, fn map_draw) + ui_native_input(ui_st, "chartmap", fn map_input) + let page = frame(ui_st, -1.0, -1.0, false, 0.0, false) + let box = page.children[0] + let pad = page.children[1] + let hold = page.children[2] + frame(ui_st, box.x + 20.0, box.y + 20.0, true, 0.0, false) + frame(ui_st, box.x + 20.0, box.y + 20.0, false, 0.0, false) + frame(ui_st, box.x + 100.0, box.y + 80.0, true, 0.0, false) + frame(ui_st, box.x + 130.0, box.y + 60.0, true, 0.0, false) + frame(ui_st, box.x + 300.0, box.y + 300.0, true, 0.0, false) + frame(ui_st, box.x + 300.0, box.y + 300.0, false, 0.0, false) + frame(ui_st, box.x + 50.0, box.y + 50.0, false, 1.0, false) + frame(ui_st, pad.x + 7.0, pad.y + 9.0, true, 0.0, false) + frame(ui_st, pad.x + 17.0, pad.y + 9.0, true, 0.0, false) + frame(ui_st, pad.x + 17.0, pad.y + 9.0, false, 0.0, false) + frame(ui_st, pad.x + 17.0, pad.y + 9.0, false, 2.0, false) + frame(ui_st, hold.x + 5.0, hold.y + 5.0, true, 0.0, false) + let down = frame(ui_st, hold.x + 5.0, hold.y + 5.0, true, 0.0, false) + let active = down.children[2].bg + let mid = said(down) + frame(ui_st, hold.x + 5.0, hold.y + 90.0, false, 0.0, false) + let off = said(frame(ui_st, -1.0, -1.0, false, 0.0, false)) + frame(ui_st, -1.0, -1.0, false, 0.0, true) + let keyed = said(frame(ui_st, -1.0, -1.0, false, 0.0, true)) + let up = frame(ui_st, -1.0, -1.0, false, 0.0, false) + print(`{ui_pointer_st.log}dragged {int(ui_pointer_st.dragged)} | {mid} active {active} | {off} | {keyed} | {said(up)} {up.children[2].bg}`) + } +} diff --git a/examples/library/ui_pointer_parts/Chart.lss b/examples/library/ui_pointer_parts/Chart.lss new file mode 100644 index 00000000..12ead9db --- /dev/null +++ b/examples/library/ui_pointer_parts/Chart.lss @@ -0,0 +1,7 @@ +.page { width: 400px; gap: 4px } +.box { width: 200px; height: 100px; position: relative } +.map { width: 200px; height: 100px } +.zoom { position: absolute; top: 10px; left: 10px; width: 30px; height: 30px; padding: 0 } +.pad { width: 100px; height: 50px } +.hold { width: 80px; height: 30px } +.hold:active { background: #ff0000 } diff --git a/examples/library/ui_pointer_parts/Chart.ludic b/examples/library/ui_pointer_parts/Chart.ludic new file mode 100644 index 00000000..e2717401 --- /dev/null +++ b/examples/library/ui_pointer_parts/Chart.ludic @@ -0,0 +1,12 @@ +# Chart.ludic - a native map with a zoom button over it, a pad that takes pointer events in markup, +# and a hold button that says when it goes down and comes up +component Chart { + state zoomed: int = 0 + state px: float = -1.0 + state dragx: float = 0.0 + state ups: int = 0 + state wh: float = 0.0 + state holding: bool = false + state held: int = 0 + state clicked: int = 0 +} diff --git a/examples/library/ui_pointer_parts/Chart.xml b/examples/library/ui_pointer_parts/Chart.xml new file mode 100644 index 00000000..ba64f3d7 --- /dev/null +++ b/examples/library/ui_pointer_parts/Chart.xml @@ -0,0 +1,9 @@ +
+
+ + +
+
+ +

zoomed {zoomed} pad {px} {dragx} {ups} {wh} hold {holding} {held} {clicked}

+
diff --git a/examples/library/ui_pointer_through.ludic b/examples/library/ui_pointer_through.ludic new file mode 100644 index 00000000..a2c5e69e --- /dev/null +++ b/examples/library/ui_pointer_through.ludic @@ -0,0 +1,55 @@ +# ui_pointer_through.ludic - a screen whose root
holds only a positioned panel (a component's +# root round a modal) has no size of its own; the pointer looks through it, so the panel's button is +# pressed and hovered - once nothing under it was. A select's < and > answer :hover as elements do. +import "ludic.ui" +program UiPointerThrough { + numbers float + state ThroughState { + got: int = 0 + v: int = 1 + } + view V (through_st: mut ThroughState) { + v = through_st.v + on hit() { through_st.got += 1 } + on set(x: int) { through_st.v = x } + } + const PAGE: string = "
" + function frame(ui_st: mut UiState, x: float, y: float, down: bool) -> UiNode { + let i = new UiInput + i.x = x + i.y = y + i.down = down + ui_input(ui_st, i) + ui_show(ui_st, "s", view_v(), 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "s", view_v()) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + function find(n: UiNode, tag: string, cls: string) -> UiNode { + var has = cls == "" + for i in 0 .. len(n.classes) { if n.classes[i] == cls { has = true } } + if n.tag == tag and has { return n } + for i in 0 .. len(n.children) { + let f = find(n.children[i], tag, cls) + if f != null { return f } + } + return null + } + entry (ui_st: mut UiState, through_st: ThroughState) { + ui_backend(ui_st, new UiBackend) + ui_load_text(ui_st, PAGE, "s.xml") + var root = frame(ui_st, -1.0, -1.0, false) + let b = find(root, "button", "") + let x = b.x + b.cw / 2.0 + let y = b.y + b.ch / 2.0 + frame(ui_st, x, y, false) + root = frame(ui_st, x, y, false) + let hover = ui_hex(find(root, "button", "").bg) + frame(ui_st, x, y, true) + frame(ui_st, x, y, false) + let nx = find(root, "span", "ui-next") + frame(ui_st, nx.x + nx.cw / 2.0, nx.y + nx.ch / 2.0, false) + root = frame(ui_st, nx.x + nx.cw / 2.0, nx.y + nx.ch / 2.0, false) + print(`pressed {through_st.got}, hovered {hover}; > hovered {find(root, "span", "ui-next").hovered}`) + } +} diff --git a/examples/library/ui_popover.ludic b/examples/library/ui_popover.ludic new file mode 100644 index 00000000..4aafafdd --- /dev/null +++ b/examples/library/ui_popover.ludic @@ -0,0 +1,59 @@ +# ui_popover.ludic — HTML's popover and progress in ludic.ui: a button opens a popover of verbs, the +# keyboard stays inside it, a press outside or Esc closes it, and a progress bar fills to its value. +import "ludic.ui" +import "ui_popover_parts/Verbs.ludic" +program UiPopover { + numbers float + function frame(ui_st: mut UiState, i: UiInput) -> UiNode { + ui_input(ui_st, i) + ui_show(ui_st, "Verbs", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "Verbs", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + function click(ui_st: mut UiState, x: float, y: float) -> void { + let d = new UiInput + d.x = x + d.y = y + d.down = true + frame(ui_st, d) + let u = new UiInput + u.x = x + u.y = y + frame(ui_st, u) + } + function said(root: UiNode) -> string { + let page = root.children[0] + return page.children[len(page.children) - 1].text + } + entry (ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + var root = frame(ui_st, new UiInput) + let cell = root.children[0].children[0] + click(ui_st, cell.x + 2.0, cell.y + 2.0) + root = frame(ui_st, new UiInput) + let tab = new UiInput + tab.tab = true + frame(ui_st, tab) + frame(ui_st, tab) + root = frame(ui_st, tab) + let enter = new UiInput + enter.enter = true + root = frame(ui_st, enter) + root = frame(ui_st, new UiInput) + let first = said(root) + click(ui_st, cell.x + 2.0, cell.y + 2.0) + root = frame(ui_st, new UiInput) + let opened = said(root) + click(ui_st, 390.0, 390.0) + root = frame(ui_st, new UiInput) + let outside = said(root) + click(ui_st, cell.x + 2.0, cell.y + 2.0) + let esc = new UiInput + esc.escape = true + frame(ui_st, esc) + root = frame(ui_st, new UiInput) + let bar = root.children[0].children[1] + print(`{first} | {opened} | {outside} | {said(root)} | fill {int(bar.children[0].cw)} of {int(bar.cw)}`) + } +} diff --git a/examples/library/ui_popover_align.ludic b/examples/library/ui_popover_align.ludic new file mode 100644 index 00000000..45b8c6c2 --- /dev/null +++ b/examples/library/ui_popover_align.ludic @@ -0,0 +1,20 @@ +# ui_popover_align.ludic — anchored popovers aligned along their side (start, center, end), kept +# within a named container (within="panel") and, by default, within the scroll box they are in, +# flipping to the other side against it rather than against the screen. +import "ludic.ui" +import "ui_popover_align_parts/Menus.ludic" +program UiPopoverAlign { + numbers float + function at(n: UiNode, base: UiNode) -> string { return `{int(n.x - base.x)},{int(n.y - base.y)}` } + entry (ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + ui_show(ui_st, "Menus", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "Menus", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + let panel = root.children[0].children[0] + let list = root.children[0].children[1] + let b = list.children[1] + let p3 = list.children[2] + print(`centred, flipped in the panel {at(panel.children[1], panel)} | end below {at(panel.children[2], panel)} | above b in the list {int(p3.y + p3.ch - b.y)} inside {p3.y >= list.y}`) + } +} diff --git a/examples/library/ui_popover_align_parts/Menus.lss b/examples/library/ui_popover_align_parts/Menus.lss new file mode 100644 index 00000000..136750a7 --- /dev/null +++ b/examples/library/ui_popover_align_parts/Menus.lss @@ -0,0 +1,8 @@ +.page { width: 400px; height: 400px } +.panel { width: 200px; height: 150px; position: relative } +.a { position: absolute; left: 150px; top: 50px; width: 40px; height: 20px; padding: 0 } +.p1 { width: 60px; height: 40px } +.p2 { width: 80px; height: 30px } +.list { overflow: auto; width: 150px; height: 100px } +.gap { height: 60px } +.p3 { width: 50px; height: 40px } diff --git a/examples/library/ui_popover_align_parts/Menus.ludic b/examples/library/ui_popover_align_parts/Menus.ludic new file mode 100644 index 00000000..3269819a --- /dev/null +++ b/examples/library/ui_popover_align_parts/Menus.ludic @@ -0,0 +1,3 @@ +# Menus.ludic - popovers anchored inside a panel and inside a scroll box, aligned along their side +component Menus { +} diff --git a/examples/library/ui_popover_align_parts/Menus.xml b/examples/library/ui_popover_align_parts/Menus.xml new file mode 100644 index 00000000..bb735bfb --- /dev/null +++ b/examples/library/ui_popover_align_parts/Menus.xml @@ -0,0 +1,12 @@ +
+
+ +
+
+
+
+

x

+ +
+
+
diff --git a/examples/library/ui_popover_mouse.ludic b/examples/library/ui_popover_mouse.ludic new file mode 100644 index 00000000..921a0864 --- /dev/null +++ b/examples/library/ui_popover_mouse.ludic @@ -0,0 +1,52 @@ +# ui_popover_mouse.ludic — a popover takes the mouse: its own button, at the same pixels as the grid +# cell under it, is the one a click presses; a click outside it (on that cell, or on nothing) closes +# it and presses nothing else. +import "ludic.ui" +import "ui_popover_mouse_parts/Grid.ludic" +program UiPopoverMouse { + numbers float + function frame(ui_st: mut UiState, i: UiInput) -> UiNode { + ui_input(ui_st, i) + ui_show(ui_st, "Grid", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "Grid", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + function click(ui_st: mut UiState, x: float, y: float) -> string { + let d = new UiInput + d.x = x + d.y = y + d.down = true + frame(ui_st, d) + let u = new UiInput + u.x = x + u.y = y + frame(ui_st, u) + let root = frame(ui_st, new UiInput) + let page = root.children[0] + return page.children[len(page.children) - 1].text + } + function find(n: UiNode, id: string) -> UiNode { + if n.id == id { return n } + for i in 0 .. len(n.children) { + let f = find(n.children[i], id) + if f != null { return f } + } + return null + } + entry (ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + frame(ui_st, new UiInput) + let opened = click(ui_st, 20.0, 20.0) + let root = frame(ui_st, new UiInput) + let drop = find(root, "drop") + let cell = find(root, "cell") + let over = drop.x >= cell.x and drop.y >= cell.y and drop.x + drop.cw <= cell.x + cell.cw + let dropped = click(ui_st, drop.x + drop.cw / 2.0, drop.y + drop.ch / 2.0) + click(ui_st, 20.0, 20.0) + let on_cell = click(ui_st, 180.0, 90.0) + click(ui_st, 20.0, 20.0) + let on_nothing = click(ui_st, 390.0, 390.0) + print(`over the cell {over} | {opened} | {dropped} | {on_cell} | {on_nothing}`) + } +} diff --git a/examples/library/ui_popover_mouse_parts/Grid.lss b/examples/library/ui_popover_mouse_parts/Grid.lss new file mode 100644 index 00000000..b80c6af9 --- /dev/null +++ b/examples/library/ui_popover_mouse_parts/Grid.lss @@ -0,0 +1,3 @@ +.page { width: 300px; height: 300px } +.cell { width: 200px; height: 100px } +.verbs { top: 10px; left: 10px; width: 120px } diff --git a/examples/library/ui_popover_mouse_parts/Grid.ludic b/examples/library/ui_popover_mouse_parts/Grid.ludic new file mode 100644 index 00000000..67823d5f --- /dev/null +++ b/examples/library/ui_popover_mouse_parts/Grid.ludic @@ -0,0 +1,8 @@ +# Grid.ludic - a grid cell with a popover of verbs laid over it, written BEFORE the cell, so the cell +# comes later in the tree and would win any hit test that went by the order things are drawn in +component Grid { + state open: bool = false + state opened: int = 0 + state dropped: int = 0 + state closed: int = 0 +} diff --git a/examples/library/ui_popover_mouse_parts/Grid.xml b/examples/library/ui_popover_mouse_parts/Grid.xml new file mode 100644 index 00000000..e119d580 --- /dev/null +++ b/examples/library/ui_popover_mouse_parts/Grid.xml @@ -0,0 +1,9 @@ +
+ +
+ +
+
+ +

opened {opened} dropped {dropped} closed {closed} {open}

+
diff --git a/examples/library/ui_popover_parts/Verbs.lss b/examples/library/ui_popover_parts/Verbs.lss new file mode 100644 index 00000000..ae965783 --- /dev/null +++ b/examples/library/ui_popover_parts/Verbs.lss @@ -0,0 +1,3 @@ +.page { width: 300px; height: 200px; gap: 4px } +.verbs { top: 40px; left: 10px; width: 120px } +progress { width: 200px } diff --git a/examples/library/ui_popover_parts/Verbs.ludic b/examples/library/ui_popover_parts/Verbs.ludic new file mode 100644 index 00000000..090f1929 --- /dev/null +++ b/examples/library/ui_popover_parts/Verbs.ludic @@ -0,0 +1,6 @@ +# Verbs.ludic - a grid cell that opens a popover of verbs +component Verbs { + state open: bool = false + state used: string = "none" + state health: int = 30 +} diff --git a/examples/library/ui_popover_parts/Verbs.xml b/examples/library/ui_popover_parts/Verbs.xml new file mode 100644 index 00000000..34ec8bb4 --- /dev/null +++ b/examples/library/ui_popover_parts/Verbs.xml @@ -0,0 +1,11 @@ +
+ + +
+ + +
+
+ +

{used} {open}

+
diff --git a/examples/library/ui_react.ludic b/examples/library/ui_react.ludic new file mode 100644 index 00000000..09edaf48 --- /dev/null +++ b/examples/library/ui_react.ludic @@ -0,0 +1,61 @@ +# ui_react.ludic — ludic.ui's React side without a renderer: keyed lists keep each item's state when +# the list is reordered, names a value, context reaches through a component, named slots, +# on-mount and on-unmount, and a native element of the program's own firing on-change with a value. +import "ludic.ui" +program UiReact { + numbers float + state UiReactState { + names: []string = new []string + volume: int = 3 + mounted: int = 0 + unmounted: int = 0 + show_extra: bool = true + } + view Mixer (ui_react_st: mut UiReactState) { + names = ui_react_st.names + volume = ui_react_st.volume + extra = ui_react_st.show_extra + on set_volume(v: int) { ui_react_st.volume = v } + on mount() { ui_react_st.mounted += 1 } + on unmount() { ui_react_st.unmounted += 1 } + } + # a dial the program draws itself: it measures 40 square and, pressed, turns up by one + function dial_measure(n: UiNode, avail: float) -> void { + n.mw = 40.0 + n.mh = 40.0 + } + function dial_draw(n: UiNode) -> void { } + const PAGE: string = "\n\n

{theme}

\n
\n\n \n \n\n\n \n \n \n \n

{loud ? 'loud' : 'quiet'}

\n
\n
\n \n \n \n

extra

\n \n

checkbox {on}

\n
\n
\n" + function texts(n: UiNode, out: []string) -> void { + if n.text != "" { push(out, n.text) } + for i in 0 .. len(n.children) { texts(n.children[i], out) } + } + function frame(ui_st: mut UiState) -> UiNode { + ui_show(ui_st, "mix", view_mixer(), 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "mix", view_mixer()) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + entry (ui_react_st: mut UiReactState, ui_st: mut UiState) { + ui_native(ui_st, "dial", fn dial_measure, fn dial_draw) + push(ui_react_st.names, "bass") + push(ui_react_st.names, "drums") + ui_load_text(ui_st, PAGE, "ui/mix.xml") + var root = frame(ui_st) + ui_press(ui_st, root.children[2]) + ui_react_st.names[0] = "drums" + ui_react_st.names[1] = "bass" + root = frame(ui_st) + let dial = root.children[3] + ui_event(ui_st, dial, "change", Value.int(ui_react_st.volume + 4)) + ui_event(ui_st, root.children[4], "change", Value.bool(1)) + ui_react_st.show_extra = false + root = frame(ui_st) + let out = new []string + texts(root, out) + var joined = "" + for i in 0 .. len(out) { joined = joined + out[i] + "|" } + print(joined) + print(`volume {ui_react_st.volume}; dial {int(dial.cw)}x{int(dial.ch)}; mounted {ui_react_st.mounted}, unmounted {ui_react_st.unmounted}`) + } +} diff --git a/examples/library/ui_reload.ludic b/examples/library/ui_reload.ludic new file mode 100644 index 00000000..9084d431 --- /dev/null +++ b/examples/library/ui_reload.ludic @@ -0,0 +1,29 @@ +# ui_reload.ludic — a template read from disk, changed there, and read again by ui_reload: the new +# text shows at once, and the screen's state (a counter pressed before the change) is kept. +import "ludic.ui" +program UiReload { + numbers float + view Note { + title = "note" + } + const PATH: string = "build/ui_reload_example.xml" + const V1: string = "" + const V2: string = "

{title} again {n}

" + function shown(ui_st: mut UiState) -> string { + let root: UiNode = ui_nodes(ui_st, "note", view_note()) + let first = root.children[0] + if len(first.children) > 0 { return first.children[0].text } + return first.text + } + entry (ui_st: mut UiState) { + Fs.write_text(PATH, V1) + ui_load(ui_st, PATH) + let root: UiNode = ui_nodes(ui_st, "note", view_note()) + ui_press(ui_st, root.children[0]) + let before = shown(ui_st) + let same = ui_reload(ui_st) + Fs.write_text(PATH, V2) + let changed = ui_reload(ui_st) + print(`{before} | reloaded {same} then {changed} | {shown(ui_st)}`) + } +} diff --git a/examples/library/ui_remount.ludic b/examples/library/ui_remount.ludic new file mode 100644 index 00000000..ed607165 --- /dev/null +++ b/examples/library/ui_remount.ludic @@ -0,0 +1,30 @@ +# ui_remount.ludic - a component that comes and goes: mounted again, it starts as new (its state back +# to its defaults), and a thousand comings and goings hold the heap flat, because an unmounted +# instance is put back and mounted again rather than a record, props and model made each time +import "ludic.ui" +import "ui_remount_parts/Counter.ludic" +import "ui_remount_parts/Shown.ludic" +program UiRemount { + numbers float + function frame(ui_st: mut UiState) -> UiNode { + let root: UiNode = ui_nodes(ui_st, "Shown", null) + ui_place(ui_st, root, 0.0, 0.0, 300.0, 200.0) + return root + } + entry (ui_st: mut UiState) { + var root = frame(ui_st) + let c = root.children[0].children[1] + ui_press(ui_st, c.children[1]) + ui_press(ui_st, c.children[1]) + root = frame(ui_st) + let before = root.children[0].children[1].children[0].text + var at = 0 + for i in 0 .. 2000 { + ui_press(ui_st, root.children[0].children[0]) + root = frame(ui_st) + if i == 99 { at = Os.heap_bytes() } + } + let grew = Os.heap_bytes() - at + print(`{before} | {root.children[0].children[1].children[0].text} | heap {grew}`) + } +} diff --git a/examples/library/ui_remount_parts/Counter.lss b/examples/library/ui_remount_parts/Counter.lss new file mode 100644 index 00000000..e69de29b diff --git a/examples/library/ui_remount_parts/Counter.ludic b/examples/library/ui_remount_parts/Counter.ludic new file mode 100644 index 00000000..57112f96 --- /dev/null +++ b/examples/library/ui_remount_parts/Counter.ludic @@ -0,0 +1,9 @@ +# Counter.ludic - a counter that starts at 0 each time it is mounted +component Counter { + prop label: string + prop step: int = 1 + state count: int = 0 + doubled: int = count * 2 + function big() -> bool { return count > 3 } + on bump() { count += step } +} diff --git a/examples/library/ui_remount_parts/Counter.xml b/examples/library/ui_remount_parts/Counter.xml new file mode 100644 index 00000000..617de5aa --- /dev/null +++ b/examples/library/ui_remount_parts/Counter.xml @@ -0,0 +1,5 @@ +
+

{label}: {count} ({doubled}){big() ? ' big' : ''}

+ + +
diff --git a/examples/library/ui_remount_parts/Shown.ludic b/examples/library/ui_remount_parts/Shown.ludic new file mode 100644 index 00000000..8d808144 --- /dev/null +++ b/examples/library/ui_remount_parts/Shown.ludic @@ -0,0 +1,5 @@ +# Shown.ludic - a counter shown or not: each time it comes back it is a new counter +component Shown { + state on: bool = true + on flip() { on = not on } +} diff --git a/examples/library/ui_remount_parts/Shown.xml b/examples/library/ui_remount_parts/Shown.xml new file mode 100644 index 00000000..cb93e733 --- /dev/null +++ b/examples/library/ui_remount_parts/Shown.xml @@ -0,0 +1,4 @@ +
+ + +
diff --git a/examples/library/ui_scroll_hold.ludic b/examples/library/ui_scroll_hold.ludic new file mode 100644 index 00000000..70e2811a --- /dev/null +++ b/examples/library/ui_scroll_hold.ludic @@ -0,0 +1,32 @@ +# ui_scroll_hold.ludic — scroll-top holds a box at an offset: one follows its state, which on-scroll +# keeps in step with the wheel; one stays where it is held; and ui_scroll_set moves another once. +import "ludic.ui" +import "ui_scroll_hold_parts/Held.ludic" +program UiScrollHold { + numbers float + function frame(ui_st: mut UiState, x: float, y: float, wheel: float) -> UiNode { + let i = new UiInput + i.x = x + i.y = y + i.wheel = wheel + ui_input(ui_st, i) + ui_show(ui_st, "Held", null, 0.0, 0.0, 400.0, 600.0) + let root: UiNode = ui_nodes(ui_st, "Held", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 600.0) + return root.children[0] + } + function at(p: UiNode) -> string { return `{int(p.children[0].scroll_y)} {int(p.children[1].scroll_y)} {int(p.children[2].scroll_y)} {p.children[3].text}` } + entry (ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + let p = frame(ui_st, -1.0, -1.0, 0.0) + let first = at(frame(ui_st, -1.0, -1.0, 0.0)) + frame(ui_st, p.children[0].x + 5.0, p.children[0].y + 5.0, 1.0) + let followed = at(frame(ui_st, -1.0, -1.0, 0.0)) + frame(ui_st, p.children[1].x + 5.0, p.children[1].y + 5.0, -1.0) + let fixed = at(frame(ui_st, -1.0, -1.0, 0.0)) + ui_scroll_set(ui_st, "free", 70.0) + let set = at(frame(ui_st, -1.0, -1.0, 0.0)) + frame(ui_st, p.children[2].x + 5.0, p.children[2].y + 5.0, 1.0) + print(`{first} | {followed} | {fixed} | {set} | {at(frame(ui_st, -1.0, -1.0, 0.0))}`) + } +} diff --git a/examples/library/ui_scroll_hold_parts/Held.lss b/examples/library/ui_scroll_hold_parts/Held.lss new file mode 100644 index 00000000..3d39fcdf --- /dev/null +++ b/examples/library/ui_scroll_hold_parts/Held.lss @@ -0,0 +1,3 @@ +.page { width: 400px; gap: 4px } +.list { overflow: auto; width: 200px; height: 100px } +p { height: 40px } diff --git a/examples/library/ui_scroll_hold_parts/Held.ludic b/examples/library/ui_scroll_hold_parts/Held.ludic new file mode 100644 index 00000000..a3c2c779 --- /dev/null +++ b/examples/library/ui_scroll_hold_parts/Held.ludic @@ -0,0 +1,5 @@ +# Held.ludic - three scroll boxes: one held at its state's offset and told when it moves, one held +# at a fixed offset, and one the program moves once +component Held { + state top: float = 120.0 +} diff --git a/examples/library/ui_scroll_hold_parts/Held.xml b/examples/library/ui_scroll_hold_parts/Held.xml new file mode 100644 index 00000000..94d975cb --- /dev/null +++ b/examples/library/ui_scroll_hold_parts/Held.xml @@ -0,0 +1,6 @@ +
+

{r}

+

{r}

+

{r}

+

top {top}

+
diff --git a/examples/library/ui_scrollbar.ludic b/examples/library/ui_scrollbar.ludic new file mode 100644 index 00000000..e7756500 --- /dev/null +++ b/examples/library/ui_scrollbar.ludic @@ -0,0 +1,54 @@ +# ui_scrollbar.ludic — a scroll box's bar held and dragged: a press on the thumb holds it where it +# was taken, the pointer held moves it in proportion, letting go leaves it; a press on the track +# jumps the thumb's middle there; the wheel still scrolls; and the rows under the bar are never pressed. +import "ludic.ui" +import "ui_scrollbar_parts/Long.ludic" +program UiScrollbar { + numbers float + state UiScrollbarState { + bx: float = 0.0 + by: float = 0.0 + } + function frame(ui_st: mut UiState, i: UiInput) -> UiNode { + ui_input(ui_st, i) + ui_show(ui_st, "Long", null, 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "Long", null) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root.children[0] + } + # the pointer at (bx + x, by + y), held or not; how far the box is scrolled then + function at(ui_scrollbar_st: UiScrollbarState, ui_st: mut UiState, x: float, y: float, down: bool) -> int { + let i = new UiInput + i.x = ui_scrollbar_st.bx + x + i.y = ui_scrollbar_st.by + y + i.down = down + return int(frame(ui_st, i).children[0].scroll_y) + } + function wheel(ui_scrollbar_st: UiScrollbarState, ui_st: mut UiState, x: float, y: float) -> int { + let i = new UiInput + i.x = ui_scrollbar_st.bx + x + i.y = ui_scrollbar_st.by + y + i.wheel = 1.0 + frame(ui_st, i) + return int(frame(ui_st, new UiInput).children[0].scroll_y) + } + entry (ui_scrollbar_st: mut UiScrollbarState, ui_st: mut UiState) { + ui_backend(ui_st, new UiBackend) + let list = frame(ui_st, new UiInput).children[0] + ui_scrollbar_st.bx = list.x + ui_scrollbar_st.by = list.y + let grab = at(ui_scrollbar_st, ui_st, 195.0, 10.0, true) + let moved = at(ui_scrollbar_st, ui_st, 195.0, 40.0, true) + let far = at(ui_scrollbar_st, ui_st, 195.0, 300.0, true) + let back = at(ui_scrollbar_st, ui_st, 195.0, 25.0, true) + let kept = at(ui_scrollbar_st, ui_st, 195.0, 25.0, false) + at(ui_scrollbar_st, ui_st, 0.0 - ui_scrollbar_st.bx - 1.0, 0.0 - ui_scrollbar_st.by - 1.0, false) + let jump = at(ui_scrollbar_st, ui_st, 195.0, 80.0, true) + at(ui_scrollbar_st, ui_st, 195.0, 80.0, false) + let wheeled = wheel(ui_scrollbar_st, ui_st, 50.0, 50.0) + at(ui_scrollbar_st, ui_st, 50.0, 20.0, true) + at(ui_scrollbar_st, ui_st, 50.0, 20.0, false) + let page = frame(ui_st, new UiInput) + print(`grab {grab} moved {moved} far {far} back {back} kept {kept} | jump {jump} | wheel {wheeled} | {page.children[1].text}`) + } +} diff --git a/examples/library/ui_scrollbar_parts/Long.lss b/examples/library/ui_scrollbar_parts/Long.lss new file mode 100644 index 00000000..5cee44f2 --- /dev/null +++ b/examples/library/ui_scrollbar_parts/Long.lss @@ -0,0 +1,3 @@ +.page { width: 300px; height: 300px } +.list { overflow: auto; width: 200px; height: 100px } +.row { width: fill; height: 40px; padding: 0; border-radius: 0 } diff --git a/examples/library/ui_scrollbar_parts/Long.ludic b/examples/library/ui_scrollbar_parts/Long.ludic new file mode 100644 index 00000000..bbdf2132 --- /dev/null +++ b/examples/library/ui_scrollbar_parts/Long.ludic @@ -0,0 +1,5 @@ +# Long.ludic - a box of ten rows, four times taller than it shows; each row is a button as wide as +# the box, so the scrollbar sits over them +component Long { + state pressed: int = 0 +} diff --git a/examples/library/ui_scrollbar_parts/Long.xml b/examples/library/ui_scrollbar_parts/Long.xml new file mode 100644 index 00000000..08daf0ea --- /dev/null +++ b/examples/library/ui_scrollbar_parts/Long.xml @@ -0,0 +1,4 @@ +
+
+

pressed {pressed}

+
diff --git a/examples/library/ui_select_arrows.ludic b/examples/library/ui_select_arrows.ludic new file mode 100644 index 00000000..20352a2a --- /dev/null +++ b/examples/library/ui_select_arrows.ludic @@ -0,0 +1,51 @@ +# ui_select_arrows.ludic - a select is a cycler: a press let go on its < steps back and on its > +# (or anywhere else on it, or Enter) forward. The < once stepped forward too. +import "ludic.ui" +program UiSelectArrows { + numbers float + state SelState { v: int = 1 } + view V (sel_st: mut SelState) { + v = sel_st.v + on set(x: int) { sel_st.v = x } + } + const PAGE: string = "" + function frame(ui_st: mut UiState, x: float, y: float, down: bool) -> UiNode { + let i = new UiInput + i.x = x + i.y = y + i.down = down + ui_input(ui_st, i) + ui_show(ui_st, "s", view_v(), 0.0, 0.0, 400.0, 400.0) + let root: UiNode = ui_nodes(ui_st, "s", view_v()) + ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0) + return root + } + function part(n: UiNode, cls: string) -> UiNode { + for i in 0 .. len(n.children) { + if List.contains(n.children[i].classes, cls) { return n.children[i] } + let f = part(n.children[i], cls) + if f != null { return f } + } + return null + } + function click(ui_st: mut UiState, cls: string) -> void { + let root = frame(ui_st, -1.0, -1.0, false) + let p = part(root, cls) + let x = p.x + p.cw / 2.0 + let y = p.y + p.ch / 2.0 + frame(ui_st, x, y, false) + frame(ui_st, x, y, true) + frame(ui_st, x, y, false) + frame(ui_st, x, y, false) + } + entry (ui_st: mut UiState, sel_st: SelState) { + ui_backend(ui_st, new UiBackend) + ui_load_text(ui_st, PAGE, "s.xml") + frame(ui_st, -1.0, -1.0, false) + click(ui_st, "ui-prev") + let a = sel_st.v + click(ui_st, "ui-next") + click(ui_st, "ui-next") + print(`prev {a} next next {sel_st.v}`) + } +} diff --git a/examples/library/ui_select_disabled.ludic b/examples/library/ui_select_disabled.ludic new file mode 100644 index 00000000..2adba55d --- /dev/null +++ b/examples/library/ui_select_disabled.ludic @@ -0,0 +1,55 @@ +# ui_select_disabled.ludic -