diff --git a/CLAUDE.md b/CLAUDE.md index 4b37f06e6..083b83e71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -235,6 +235,7 @@ Full migration table (when reading older docs that say `var inscope` or `<-` for - String builder requires `unsafe` or `options persistent_heap` if returned - Tuple field access: `t._0`, `t._1`, `t._2` - Annotations: `[export]`, `[test]`; `options no_aot`, `options rtti` +- **`options` are MODULE-LOCAL for pass-macros** (`[lint_macro]` / `AstPassMacro`). The macro fires once per module in the require chain, reading `prog._options` from THAT module's options table — not the program-root's. So `options _my_lint_off = true` in `foo.das` suppresses YOUR lint in `foo`, but `require foo` from `bar.das` does not inherit the flag — `bar` gets linted unless it sets its own. Don't confuse with runtime options (`gc`, `multiple_contexts`, `persistent_heap`, `rtti`) which DO unify across the program codegen and effectively cascade up to consumers - **Visibility is a prefix keyword, not an annotation:** `def private foo()`, `struct private Foo { ... }`, `enum private E { ... }`, `variable private x = 0`, `alias private X = Y`. There is **no** `[private]` annotation — it's a grammar error - **Field/variable annotations use `@name` only:** `@safe_when_uninitialized at : LineInfo`, `@sql_primary_key id : int64`, `@do_not_delete ctx : Context?`. The `[name]` form is reserved for struct/function/global-level annotations and does NOT parse on a struct field - `require` uses forward slash: `require daslib/linq` — NOT backslash diff --git a/modules/dasGlfw/dasglfw/glfw_live.das b/modules/dasGlfw/dasglfw/glfw_live.das index d0a8898e7..71318e4fe 100644 --- a/modules/dasGlfw/dasglfw/glfw_live.das +++ b/modules/dasGlfw/dasglfw/glfw_live.das @@ -22,6 +22,7 @@ require glfw/glfw_boost require opengl/opengl_boost require opengl/opengl_cache require daslib/archive +require daslib/clargs require daslib/json require daslib/json_boost require daslib/utf8_utils @@ -32,9 +33,29 @@ require strings var public live_window : GLFWwindow? +// Lazy CLI-flag cache. `--no-hdpi-framebuffer` after `--` asks the backend +// to open a window whose framebuffer matches the requested logical size, +// even on retina / high-DPI displays. Used by APNG recording tools to keep +// capture pixel counts (and PNG encoder workload) small. +var private g_no_hdpi_parsed : bool = false +var private g_no_hdpi : bool = false + +def private ensure_no_hdpi_parsed() { + return if (g_no_hdpi_parsed) + g_no_hdpi_parsed = true + let args <- get_user_args() + let raw = find_bool_flag_raw_value(args, "--no-hdpi-framebuffer") + g_no_hdpi = (raw |> unwrap_or("false")) == "true" +} + def public live_create_window(title : string; width, height : int) : GLFWwindow? { //! Creates a GLFW window (or reuses the preserved one on reload). //! Call this from init(). + //! + //! Pass ``--no-hdpi-framebuffer`` (after the daslang ``--`` separator) + //! to force the framebuffer to match the requested logical dimensions + //! on high-DPI displays — used by APNG recording tools to keep capture + //! pixel counts manageable. // Already have a window (restored from reload) return live_window if (live_window != null) if (glfwInit() == 0) { @@ -42,6 +63,15 @@ def public live_create_window(title : string; width, height : int) : GLFWwindow? } glfwInitOpenGL(3, 3) glfwWindowHint(int(GLFW_SAMPLES), 4) + ensure_no_hdpi_parsed() + if (g_no_hdpi) { + // macOS: disable retina backing so framebuffer == window size in + // physical pixels. No-op on other platforms. + glfwWindowHint(int(GLFW_COCOA_RETINA_FRAMEBUFFER), 0) + // Windows: override any caller-set SCALE_TO_MONITOR=1 so DPI scaling + // doesn't multiply the requested size. No-op on macOS/Linux. + glfwWindowHint(int(GLFW_SCALE_TO_MONITOR), 0) + } live_window = glfwCreateWindow(width, height, title, null, null) if (live_window == null) { panic("glfw_live: can't create window") diff --git a/mouse-data/docs/how-do-i-audit-which-dasimgui-consumer-files-genuinely-need-options-allow-imgui-legacy-true-and-which-are-dead-pr-33-scaffolding.md b/mouse-data/docs/how-do-i-audit-which-dasimgui-consumer-files-genuinely-need-options-allow-imgui-legacy-true-and-which-are-dead-pr-33-scaffolding.md new file mode 100644 index 000000000..ecf83f6c3 --- /dev/null +++ b/mouse-data/docs/how-do-i-audit-which-dasimgui-consumer-files-genuinely-need-options-allow-imgui-legacy-true-and-which-are-dead-pr-33-scaffolding.md @@ -0,0 +1,38 @@ +--- +slug: how-do-i-audit-which-dasimgui-consumer-files-genuinely-need-options-allow-imgui-legacy-true-and-which-are-dead-pr-33-scaffolding +title: How do I audit which dasImgui consumer files genuinely need `options _allow_imgui_legacy = true` and which are dead PR #33 scaffolding I can drop? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +PR #33 (2026-05-15) bulk-added `options _allow_imgui_legacy = true` defensively to every imgui_demo file as part of flipping the lint default-on. Many of those opt-outs are now dead scaffolding — the file genuinely has no raw `imgui::*` calls, the option is suppressing nothing. + +**Audit pattern (per-file strip + dry-run + IMGUI002 count):** + +```sh +# For each candidate file: cp, strip, dry-run, count, restore. +for f in examples/imgui_demo/*.das; do + if ! grep -q "^options _allow_imgui_legacy" "$f"; then echo "= $f (no opt-out)"; continue; fi + cp "$f" "$f.bak" + sed -i '/^options _allow_imgui_legacy/d' "$f" + errs=$(daslang.exe -dry-run -project_root "$f" 2>&1 | grep -c "IMGUI002") + mv "$f.bak" "$f" + echo "$errs $f" +done | sort -n +``` + +**Found in PR #40 audit (2026-05-16):** +- 9 files report `0` IMGUI002 errors → dead scaffolding, drop the opt-out: `app_console`, `app_custom_rendering`, `app_dockspace`, `app_documents`, `app_small`, `main`, `popups`, `tables`, `user_guide`. +- 7 files report `2..133` IMGUI002 errors → genuine raw survivors, keep the opt-out until individually swept: `app_main_menu` (2), `about` (5), `imgui_demo` (6), `widgets` (6), `style_editor` (9), `inputs` (13), `layout` (133). + +**Why this is safe:** the lint pass is conservative — IMGUI002 is the only error class gated by `_allow_imgui_legacy`. Zero IMGUI002 errors with the option removed means the option is suppressing nothing. + +**Don't blindly drop in widget-builtin files** (`widgets/imgui_*.das`). Those carry the opt-out structurally — their wrappers ARE the bottom-of-stack and legitimately raw-call `Columns/InvisibleButton/Begin/End/Render` etc. The lint visits transitive module bodies (see card `why-does-my-lint-macro-fire-on-the-wrapper-module-that-legitimately-uses-the-forbidden-symbols-even-though-i-scope-visit-module`), so an audit on a widget builtin will return non-zero. That's correct — the opt-out stays. + +**Related cards:** +- `why-does-my-lint-macro-fire-on-the-wrapper-module-that-legitimately-uses-the-forbidden-symbols-even-though-i-scope-visit-module` — explains the per-module pass-macro scope that makes the opt-out structurally required in wrapper modules. +- `port-v1-imgui-boost-example-to-boost-v2-checklist` — the per-file sweep that converts the 7 genuine-survivor files (kills their need for the opt-out). + +## Questions +- How do I audit which dasImgui consumer files genuinely need `options _allow_imgui_legacy = true` and which are dead PR #33 scaffolding I can drop? diff --git a/mouse-data/docs/how-do-i-verify-a-dasimgui-apng-recording-is-correct-without-playing-it-in-a-viewer-screenshot-live-process-extract-individual-f.md b/mouse-data/docs/how-do-i-verify-a-dasimgui-apng-recording-is-correct-without-playing-it-in-a-viewer-screenshot-live-process-extract-individual-f.md new file mode 100644 index 000000000..b6c83da9c --- /dev/null +++ b/mouse-data/docs/how-do-i-verify-a-dasimgui-apng-recording-is-correct-without-playing-it-in-a-viewer-screenshot-live-process-extract-individual-f.md @@ -0,0 +1,86 @@ +--- +slug: how-do-i-verify-a-dasimgui-apng-recording-is-correct-without-playing-it-in-a-viewer-screenshot-live-process-extract-individual-f +title: how do I verify a dasImgui APNG recording is correct without playing it in a viewer — screenshot live process + extract individual frames +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +# Two ground-truth probes for dasImgui recordings + +When iterating on a `record_*.das` driver and the resulting APNG looks +wrong (no cursor, wrong menu state, mistimed narrate), don't rely on +Boris's eyeball — instrument it: + +## Probe 1: live-process screenshot (state at *this moment*) + +While daslang-live is running, post arbitrary synth events via MCP, +then capture the framebuffer to a PNG you can read in Claude Code: + + mcp__daslang__live_command name="imgui_mouse_play" args='{"events":[ + {"t_ms":0,"kind":"move","x":50,"y":15}, + {"t_ms":1000,"kind":"move","x":300,"y":300}]}' + mcp__daslang__live_command name="imgui_mouse_status" # cursor_owned? + mcp__daslang__live_command name="screenshot" args='{"file":"/path/diag.png"}' + +Then `Read` the PNG. This shows what daslang-live is rendering RIGHT NOW +— cursor sprite included, foreground draw list included, narrate +callouts included. If the screenshot shows the cursor but the APNG +doesn't, the bug is in the recording path, not the render path. If +neither shows it, the synth IO isn't draining (see related card on +harness_apply_synth_io). + +Useful for: confirming menu opens on click, finding empirical pixel +coords for unregistered widgets, verifying narrate visibility. + +## Probe 2: extract individual frames from the APNG with ffmpeg + +APNG inspection without a viewer: + + ffmpeg -i scene.apng -vf "select=eq(n\,80)" -frames:v 1 \ + -update 1 frame80.png -y + +Notes: +- `-update 1` is REQUIRED for single-frame output; otherwise ffmpeg + expects a `%d`-style pattern and errors out. +- `select=eq(n\,N)` picks frame N (0-based). Comma needs the backslash. +- Picking N values spread across the recording (e.g. 60, 200, 400, + 600, 800, 950 for a 1000-frame recording) gives you a fast story- + arc check without watching the whole thing. + +Then `Read` each PNG to verify each stage looks right. + +## Workflow + +1. Iterate driver `record_*.das` → run against live host. +2. After each record_stop, extract 5-6 frames spanning the timeline. +3. Read frames to verify: cursor visible? trail visible? menu open + when narrated? narrate text matches visual? +4. If anything's off, kill daslang-live (NEVER reuse a dirty session + — earlier interactive probes contaminate menu state), restart + fresh, re-record. + +## Reader pacing rule + +Recordings for tutorials need TIME to be read. Per stage: + + let NARRATE_FRAMES = 240 // 4.0s visible at 60 fps app + let READ_MS = 5000u // 5s read + 1s gap before next action + let SETTLE_MS = 1500u // 1.5s cursor settle before next narrate + let RESULT_MS = 2000u // 2s action result dwell + +Set `record_start max_seconds` ≥ sum of (settle + read + result) per +stage + slack. Recordings that fit in 30s are usually too dense. + +## Related + +- `mcp__daslang__live_command help` — list all live commands incl. + `screenshot`, `imgui_mouse_status`, `imgui_snapshot`. +- `tests/integration/record_with_id.das` — canonical recording template + (uses `live_*` lifecycle, not `harness_*`). +- `tests/integration/record_imgui_demo_about.das` / + `record_imgui_demo_app_main_menu.das` — drivers using the pacing + constants above against `harness_*.das` hosts. + +## Questions +- how do I verify a dasImgui APNG recording is correct without playing it in a viewer — screenshot live process + extract individual frames diff --git a/mouse-data/docs/imgui-harness-lint-forbidden-modules-harness001.md b/mouse-data/docs/imgui-harness-lint-forbidden-modules-harness001.md new file mode 100644 index 000000000..5dff6f33d --- /dev/null +++ b/mouse-data/docs/imgui-harness-lint-forbidden-modules-harness001.md @@ -0,0 +1,38 @@ +--- +slug: imgui-harness-lint-forbidden-modules-harness001 +title: What modules does imgui_harness_lint forbid (HARNESS001) and is imgui_app one of them? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +`imgui_harness_lint` is default-on for every file that does `require imgui/imgui_harness`. It blocks calls into exactly 5 modules: + +``` +glfw_boost +opengl_boost +glfw_live +opengl_live +imgui_live +``` + +Source: `D:/DASPKG/dasImgui/widgets/imgui_harness_lint.das:33-35` (`FORBIDDEN_MODULES` table). + +**`imgui_app` is NOT in the list.** The harness imports `imgui_app` privately, so consumers still need their own `require imgui_app` to call `ImGui_ImplOpenGL3_RenderDrawData` (visibility), but no HARNESS001 fires — that's deliberate, because `imgui_app` is the bridge layer between ImGui and OpenGL that legitimate split-harness consumers need to drain DrawData. If `imgui_app` were forbidden, the split-harness recipe for custom 3D + ImGui overlay (separate card) would be uncodeable. + +Error code: **50503**, prefix **HARNESS001**, severity `macro_error` (stops compile, not a warning). + +Message: +``` +HARNESS001: {mname}::{fname} is forbidden in files that require imgui_harness — +use the harness helpers (harness_init / harness_begin_frame / harness_new_frame / +harness_end_frame / harness_shutdown) instead. Per-file escape: +`options _allow_glfw_calls = true` (scaffolding only) +``` + +**Per-file opt-out:** `options _allow_glfw_calls = true`. Scaffolding-only — target end state is no opt-out, same pattern as PR #33's `_allow_imgui_legacy`. Use for the genuine custom 3D + ImGui overlay case; do NOT use to bypass when your code should be going through `harness_begin_frame` / `harness_end_frame`. + +**Scoping:** lint walks `prog.getThisModule` only — transitively-required modules (including `imgui_harness` itself, which legitimately calls into the backends as the wrapper) are skipped. Macro-generated bodies and macro-synthesized call sites are filtered by `fileInfo` mismatch. + +## Questions +- What modules does imgui_harness_lint forbid (HARNESS001) and is imgui_app one of them? diff --git a/mouse-data/docs/imgui-harness-split-custom-opengl-overlay-recipe.md b/mouse-data/docs/imgui-harness-split-custom-opengl-overlay-recipe.md new file mode 100644 index 000000000..442fd4a47 --- /dev/null +++ b/mouse-data/docs/imgui-harness-split-custom-opengl-overlay-recipe.md @@ -0,0 +1,58 @@ +--- +slug: imgui-harness-split-custom-opengl-overlay-recipe +title: How do I render custom OpenGL underneath an ImGui overlay when using imgui_harness? harness_end_frame does its own clear + ImGui draw call so I can't slot custom GL in. +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +`harness_end_frame` packs `end_of_frame` + `Render` + viewport/clear + `ImGui_ImplOpenGL3_RenderDrawData` + `live_end_frame` into one block. For a custom 3D scene under an ImGui controls window, you need GL between the clear and ImGui's draw drain — split the harness manually: + +```daslang +options _allow_glfw_calls = true + +require imgui/imgui_harness +require opengl/opengl_boost // gl* + create_shader_program +require glfw/glfw_boost // glfwGetTime +require live/glfw_live // live_get_framebuffer_size, live_end_frame +require imgui_app // ImGui_ImplOpenGL3_RenderDrawData +require daslib/safe_addr + +[export] def init() { + harness_init("My App", 1024, 1024) + create_gl_objects() +} + +[export] def update() { + if (!harness_begin_frame()) return + harness_new_frame() + + my_widgets() // boost-v2 window/edit_*/text calls + + var w, h : int + live_get_framebuffer_size(w, h) + glViewport(0, 0, w, h) + glClearColor(0.85f, 0.85f, 0.90f, 1.0f) + glClear(GL_COLOR_BUFFER_BIT) + my_custom_gl() // your scene draws here + + end_of_frame() + Render() + ImGui_ImplOpenGL3_RenderDrawData(GetDrawData()) + live_end_frame() +} + +[export] def shutdown() { harness_shutdown() } +``` + +Why each piece: + +- **`_allow_glfw_calls = true`** opts out of `imgui_harness_lint` (HARNESS001), which forbids direct calls into 5 windowed-backend modules (`glfw_boost`, `opengl_boost`, `glfw_live`, `opengl_live`, `imgui_live`). Scaffolding-only flag, same pattern as `_allow_imgui_legacy`. +- **Explicit backend requires** — `imgui_harness` imports the backend modules *privately*, so their symbols don't reach consumers transitively. Every `live_*`/`gl*`/`ImGui_Impl*` call you make directly needs its own `require`. +- **`imgui_app` needs the require but is NOT lint-blocked** — the harness still considers `ImGui_ImplOpenGL3_RenderDrawData` legitimate (you can't drain DrawData any other way), it's just a visibility issue. +- **`harness_init` / `harness_shutdown` still work** — they handle CreateContext, GLFW window, theme + JetBrains Mono via `live_imgui_init` (PR #39, 2026-05-16), and cleanup. No explicit `apply_daslang_theme()` call needed. + +Working example: `examples/graphics/furier_opengl_imgui_example.das` (daslang PR #2695, merged 2026-05-17). + +## Questions +- How do I render custom OpenGL underneath an ImGui overlay when using imgui_harness? harness_end_frame does its own clear + ImGui draw call so I can't slot custom GL in. diff --git a/mouse-data/docs/in-a-dasimgui-recording-driver-why-does-drag-to-app-source-target-make-the-cursor-visually-appear-at-the-destination-but-the-cli.md b/mouse-data/docs/in-a-dasimgui-recording-driver-why-does-drag-to-app-source-target-make-the-cursor-visually-appear-at-the-destination-but-the-cli.md new file mode 100644 index 000000000..2e0560ada --- /dev/null +++ b/mouse-data/docs/in-a-dasimgui-recording-driver-why-does-drag-to-app-source-target-make-the-cursor-visually-appear-at-the-destination-but-the-cli.md @@ -0,0 +1,59 @@ +--- +slug: in-a-dasimgui-recording-driver-why-does-drag-to-app-source-target-make-the-cursor-visually-appear-at-the-destination-but-the-cli +title: In a dasImgui recording driver, why does drag_to(app, source, target) make the cursor visually appear at the destination but the click never seems "held" (no drag tooltip, drag_drop target never accepts payload, slider doesn't move)? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +**`drag_to` dispatches the L1 `imgui_drag` coroutine, which calls `AddMouseButtonEvent` / `AddMousePosEvent` DIRECTLY on `io` — bypassing the synth pipeline. That races against `imgui_synth_tick`'s per-frame re-assertion of cursor pos + held-buttons.** + +`widgets/imgui_boost_runtime.das:1393-1406` `drag_coro`: + + [async] + def private drag_coro(start, endp, steps, button) { + var io & = unsafe(GetIO()) + io |> AddMousePosEvent(start.x, start.y) + io |> AddMouseButtonEvent(button, true) + await_next_frame() + for (i in range(1, steps + 1)) { + let p = start + (endp - start) * t + io |> AddMousePosEvent(p.x, p.y) + await_next_frame() + } + io |> AddMouseButtonEvent(button, false) + } + +Meanwhile every frame `apply_synth_io_override()` (live) or `harness_apply_synth_io()` (harness) calls `imgui_synth_tick`, which: + +1. Re-asserts `synth_cursor_x, synth_cursor_y` via `AddMousePosEvent` — overrides whatever `drag_coro` just emitted with the LAST `move_to`'s target +2. Re-asserts `synth_held_buttons` via `AddMouseButtonEvent` — but `drag_coro`'s press never lands in `synth_held_buttons` (different code path), so it isn't re-asserted + +End result: ImGui sees the cursor at the static `move_to` target (not the interpolated drag positions), the button-down event arrives once but the held state isn't re-asserted, so ImGui's internal drag-threshold logic never trips. Cursor sprite ends up at the target, but drag never visually "engaged." + +## Fix: use `drag_along` + `imgui_mouse_play` + +`drag_along` builds an events array; `imgui_mouse_play` queues them via the synth pipeline (`synth_held_buttons` + `synth_cursor`), which IS the drained path: + + var drag_events : array + drag_events |> drag_along(0, p_source, p_target, 1200, 400) + post_command(app, "imgui_mouse_play", JV((events = drag_events))) + sleep(uint(2200)) //! approach(400) + 100 + drag(1200) + 200 + 300 + +Verified 2026-05-17 on `examples/features/drag_drop.das` recording: drag_to → `Drops accepted: 0`. drag_along → drag tooltip "Dragging int: 42" follows cursor, TARGET highlighted on hover, `Drops accepted: 1`. + +## Why does `drag_to` exist at all? + +It works in non-synth contexts (running daslang-live without `apply_synth_io_override`, no synth IO conflicts) and for tests that don't care about the visual drag. The L1 coroutine ships with `imgui_boost_runtime`; the L2 playwright helper (`drag_along` + `imgui_mouse_play`) is the synth-pipeline-integrated path. + +**Rule of thumb:** if your recording driver is making an APNG, use `drag_along + imgui_mouse_play`. If it's a fast integration smoke that just needs the drag to fire telemetry-wise, `drag_to` is fine. + +## Related + +- `tests/integration/record_drag_drop.das` — uses the `drag_along + imgui_mouse_play` form post-fix. +- `widgets/imgui_playwright.das:92-112` — `drag_along` signature. +- `widgets/imgui_live_core.das:702-720` — `imgui_synth_tick` re-assertion logic. +- mouse card `feedback_synth_io_override_pattern` — broader synth IO discipline. + +## Questions +- In a dasImgui recording driver, why does drag_to(app, source, target) make the cursor visually appear at the destination but the click never seems "held" (no drag tooltip, drag_drop target never accepts payload, slider doesn't move)? diff --git a/mouse-data/docs/my-dasimgui-apng-recording-is-committing-as-100-mb-and-github-warns-about-file-size-how-do-i-shrink-it-without-sacrificing-reade.md b/mouse-data/docs/my-dasimgui-apng-recording-is-committing-as-100-mb-and-github-warns-about-file-size-how-do-i-shrink-it-without-sacrificing-reade.md new file mode 100644 index 000000000..3ac29de7d --- /dev/null +++ b/mouse-data/docs/my-dasimgui-apng-recording-is-committing-as-100-mb-and-github-warns-about-file-size-how-do-i-shrink-it-without-sacrificing-reade.md @@ -0,0 +1,55 @@ +--- +slug: my-dasimgui-apng-recording-is-committing-as-100-mb-and-github-warns-about-file-size-how-do-i-shrink-it-without-sacrificing-reade +title: My dasImgui APNG recording is committing as 100+ MB and GitHub warns about file size — how do I shrink it without sacrificing reader pacing? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +**Three knobs in order of cost/benefit: shrink the host window, drop fps to 20, and check whether you're recording per-frame animations.** + +Existing dasImgui APNGs sit in a 7.5 MB → 77 MB band (`visual_aids_tour.apng` is the largest committed). GitHub's CLI warns at >50 MB but doesn't block. The sweet spot for new recordings is **≤75 MB**, matching the existing ceiling. + +## Knob 1: window dimensions (biggest lever) + +File size scales roughly linearly with pixel count. The two narrative+edit_external recordings in PR 5b/5c each shrunk from 900×640 / 820×600 down to **720×480** with `FontGlobalScale = 1.15` (down from 1.4) — cut the file roughly in half. Live_create_window dimensions only take effect on init, so a `live_reload` doesn't pick up the change — full restart required. + +In the feature file: + + live_create_window("dasImgui my_tour", 720, 480) + live_imgui_init(live_window) + var io & = unsafe(GetIO()) + io.FontGlobalScale = 1.15 + +Also shrink the inside-window `SetNextWindowSize` to fit (e.g. 680×440 inside a 720×480 frame). + +## Knob 2: recording fps + +`record_start(fps = 20)` instead of `fps = 30` cuts frames by 33% → file by roughly 30%. Cursor tracking is still smooth enough for readers at 20 fps. + + post_command(app, "record_start", JV((file = OUTPUT, fps = 20, max_seconds = 75))) + +## Knob 3: per-frame animations defeat compression + +APNG uses delta compression between frames. If your feature file animates something on EVERY frame (per-tick progress_bar fill, real-time clock, rotating gauge), every frame is "different" and the delta isn't compressed — file size balloons. + +Hard to fix without changing the feature semantics, but worth knowing: a tour with one animating progress_bar will be ~30% larger than the same tour with all static widgets. + +## Combine all three + +`narrative_layout_tour.apng` started at 124 MB (900×640, fps=30, animating progress_bar). After knob 1 (→720×480) it was 90 MB. After knob 2 (→fps=20) it was 60 MB. That's the same story arc, just lighter. + +## Verify size before committing + + ls -la doc/source/_static/tutorials/your_apng.apng + +Bracket: 7.5 MB → 75 MB OK; >75 MB consider knob 1+2 above; <5 MB suspicious (probably under-paced — too few frames per stage). + +## Related + +- `tests/integration/record_boost_narrative_layout.das` / `record_edit_external.das` — both record at fps=20 for size reasons. +- `examples/tutorial/narrative_layout_tour.das` / `editing_external.das` — both shrunk to 720×480 / FontGlobalScale=1.15. +- mouse card `imgui-narrate-frames-vs-recorder-fps` — pacing constants are independent of recorder fps. + +## Questions +- My dasImgui APNG recording is committing as 100+ MB and GitHub warns about file size — how do I shrink it without sacrificing reader pacing? diff --git a/mouse-data/docs/my-dasimgui-recording-driver-calls-move-to-app-p-widget-but-the-cursor-sprite-shows-up-at-the-framebuffer-corner-instead-of-the.md b/mouse-data/docs/my-dasimgui-recording-driver-calls-move-to-app-p-widget-but-the-cursor-sprite-shows-up-at-the-framebuffer-corner-instead-of-the.md new file mode 100644 index 000000000..bd26f9ea2 --- /dev/null +++ b/mouse-data/docs/my-dasimgui-recording-driver-calls-move-to-app-p-widget-but-the-cursor-sprite-shows-up-at-the-framebuffer-corner-instead-of-the.md @@ -0,0 +1,44 @@ +--- +slug: my-dasimgui-recording-driver-calls-move-to-app-p-widget-but-the-cursor-sprite-shows-up-at-the-framebuffer-corner-instead-of-the +title: My dasImgui recording driver calls move_to(app, p_widget) but the cursor sprite shows up at the framebuffer corner instead of the widget. Snapshot has the widget. What's wrong? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +**The widget path you passed to `widget_center` doesn't exist in the snapshot. `widget_center` returns `(0, 0)` on missing paths (it `??` defaults each bbox field to `0.0f`), and `(0, 0)` is the framebuffer corner.** + +Most common cause: the feature file omits an explicit ident on the widget, so the boost `[widget]` macro falls back to a source-line:col path. Example — `narrative_layout_tour.das` had: + + text("Plain text() — one line, telemetry-visible.") + text_wrapped("text_wrapped() reflows ...") + +These register at `TOUR_WIN/:66:8` and `TOUR_WIN/:69:8`. The driver asked for `TOUR_WIN/INTRO` / `TOUR_WIN/WRAP` (the names from the file's docstring "All idents are intentionally short and stable") — got `(0, 0)` back, `move_to((0, 0))` parked the cursor at the corner. The narrate callout also resolved its `target` to the missing path and anchored near the corner with a connector line dangling to the actual widget rect. + +## Fix: explicit ident state + named-tuple value + +For `[widget]` macros, the canonical "stable ident" form is: + + text(INTRO, (text = "Plain text() — one line, telemetry-visible.")) + text_wrapped(WRAP, (text = "text_wrapped() reflows ...")) + text_colored(COLORED, (color = float4(...), text = "...")) + label_text(LABEL_VERSION, (key = "Version", value = "v2.0-detour")) + separator_text(SEP_TITLE, (text = "Display + tooltips")) + progress_bar(PROG_BAR, (fraction = g_progress, size = ..., overlay = "")) + +The first positional arg is the state ident (auto-emitted as a `var private` at module scope by the macro). The second is a NAMED-TUPLE wrapping all the actual widget params — named-tuple destructure is positional-by-tuple-field-index against the function signature (see mouse card `dasimgui-widget-named-tuple-args-positional-not-named`). + +For `[widget]`s that take a single positional arg like `text(s : string)`, you'd think `text(INTRO, "hello")` would work; in practice the macro wants the named-tuple form for consistency with multi-arg widgets — and reading `(text = "hello")` makes the call self-documenting. + +## How to detect this trap before recording + +`mcp__daslang__live_command name="imgui_snapshot"`, then grep the JSON for your driver's target paths. If you see `TOUR_WIN/:66:8`-style entries instead of `TOUR_WIN/INTRO`, the feature file needs idents. + +## Related + +- `examples/tutorial/narrative_layout_tour.das` — fixed 2026-05-17 to use explicit idents on all 8 narrative+layout widgets. +- mouse card `dasimgui-widget-named-tuple-args-positional-not-named` — why field NAMES inside `(...)` don't dispatch. +- mouse card `my-dasimgui-single-scene-harness-driver-has-no-cursor-sprite-in-the-recorded-apng-and-clicks-don-t-open-menus-what-am-i-missing` — different cause (synth IO not draining); same symptom (no visible cursor). + +## Questions +- My dasImgui recording driver calls move_to(app, p_widget) but the cursor sprite shows up at the framebuffer corner instead of the widget. Snapshot has the widget. What's wrong? diff --git a/mouse-data/docs/my-dasimgui-single-scene-harness-driver-has-no-cursor-sprite-in-the-recorded-apng-and-clicks-don-t-open-menus-what-am-i-missing.md b/mouse-data/docs/my-dasimgui-single-scene-harness-driver-has-no-cursor-sprite-in-the-recorded-apng-and-clicks-don-t-open-menus-what-am-i-missing.md new file mode 100644 index 000000000..f164fb568 --- /dev/null +++ b/mouse-data/docs/my-dasimgui-single-scene-harness-driver-has-no-cursor-sprite-in-the-recorded-apng-and-clicks-don-t-open-menus-what-am-i-missing.md @@ -0,0 +1,76 @@ +--- +slug: my-dasimgui-single-scene-harness-driver-has-no-cursor-sprite-in-the-recorded-apng-and-clicks-don-t-open-menus-what-am-i-missing +title: my dasImgui single-scene harness driver has no cursor sprite in the recorded APNG and clicks don't open menus — what am I missing? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +# harness_apply_synth_io() is the missing link + +The dasImgui harness API exposes `harness_apply_synth_io()` as an +**optional** call (per `widgets/imgui_harness.das:163` docstring). When +a harness is **driven by synth IO** — i.e. a `record_*.das` script +posting `imgui_mouse_play` / `imgui_key_play` via daslang-live — that +call is **mandatory**, not optional. Insert it between +`harness_begin_frame()` and `harness_new_frame()`: + + [export] + def update() { + if (!harness_begin_frame()) return + harness_apply_synth_io() // <-- this line + harness_new_frame() + ShowMyDemo() + harness_end_frame() + } + +## Why + +`harness_begin_frame()` calls `ImGui_ImplGlfw_NewFrame()` (windowed mode) +which polls the real GLFW mouse and writes to `io.MousePos`. Without +`harness_apply_synth_io()` after it, the GLFW poll wins the race and +overwrites whatever synth events queued. Result: + +- `synth_cursor_owned` stays `false` +- `paint_cursor_sprite` (foreground draw list) paints at `GetMousePos()` + = real OS cursor (typically off-window) → invisible in recording +- `paint_trail` same story +- synth `mouse_button` press/release events never reach widget hover + state → menus stay closed, buttons don't activate + +The diagnostic warning `synth events queued but +apply_synth_io_override() has never run` (from +`imgui_live_core.das:737`) fires only when `imgui_synth_tick` has run +ZERO times in the process. If you call `apply_synth_io_override()` once +but then SOME frames also skip it (e.g. you forgot to add it inside an +if-branch), the warning DOESN'T fire and you get silent partial drain. + +## Verification + +1. Run `mcp__daslang__live_command imgui_mouse_status` against the + harness with synth events queued. Should show `"cursor_owned": true`. +2. `mcp__daslang__live_command screenshot` to a PNG should show the + red+white cursor sprite at the synth position. +3. If both succeed but the recorded APNG still has no cursor, the + `imgui_cursor_sprite` / `imgui_mouse_trail` live_commands weren't + posted before `record_start`. Order matters: + + post_command(app, "imgui_mouse_trail", JV((enabled = true))) + post_command(app, "imgui_cursor_sprite", JV((enabled = true))) + post_command(app, "record_start", JV(...)) + // ... move_to / click_at sequence + +## Related + +- `widgets/imgui_harness.das:163` — `harness_apply_synth_io` docstring. +- `widgets/imgui_live_core.das:737` — the one-shot warning that fires + when synth events queue before any tick. +- `examples/imgui_demo/harness_about.das` — canonical example with the + call in place. +- `examples/features/*.das` — the 8 feature files that already include + it (active_widget, columns_demo, dock_basic, glfw_synth_*, + imgui_synth_*, layout_helpers). The rest skip it because their tests + don't drive synth IO. + +## Questions +- my dasImgui single-scene harness driver has no cursor sprite in the recorded APNG and clicks don't open menus — what am I missing? diff --git a/mouse-data/docs/port-v1-imgui-boost-example-to-boost-v2-checklist.md b/mouse-data/docs/port-v1-imgui-boost-example-to-boost-v2-checklist.md new file mode 100644 index 000000000..ad1586466 --- /dev/null +++ b/mouse-data/docs/port-v1-imgui-boost-example-to-boost-v2-checklist.md @@ -0,0 +1,50 @@ +--- +slug: port-v1-imgui-boost-example-to-boost-v2-checklist +title: What's the conversion checklist for porting a v1 dasImgui imgui_boost example to the boost-v2 harness + widget surface? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +Mechanical substitutions for porting any v1 `imgui_boost` example onto current dasImgui (post PR #33 default-on lint, PR #38 harness, PR #39 daslang theme). Worked example: `examples/graphics/furier_opengl_imgui_example.das` — daslang PR #2695. + +**1. Requires.** Drop `require imgui/imgui_boost` + the v1 import bundle. Replace with one line: +```daslang +require imgui/imgui_harness +require daslib/safe_addr // if you'll use the edit_* rail +``` +`imgui_harness` re-exports `imgui public` + all `imgui_boost_v2`/`imgui_widgets_builtin`/`imgui_containers_builtin`/`imgui_scope_builtin`/etc., the daslang theme module, and harness lifecycle. Backend modules (`glfw_boost`, `opengl_boost`, `imgui_app`) are required PRIVATELY — re-require explicitly only if you call them directly (custom GL case). + +**2. Init.** Drop hand-rolled `glfwInit` / `glfwCreateWindow` / `CreateContext` / `StyleColorsDark` / `FontGlobalScale` shims. Use: +```daslang +[export] def init() { harness_init("Title", 1024, 768) } +[export] def shutdown() { harness_shutdown() } +``` +Theme + JetBrains Mono auto-apply via `live_imgui_init` (PR #39, 2026-05-16). No explicit `apply_daslang_theme()` / `load_daslang_font()` call needed. + +**3. Window container.** `Begin("X", p_open, flags) ... End()` → `window(IDENT, (text = "X", closable = false, flags = ImGuiWindowFlags.None)) { ... }`. `IDENT` is a daslang identifier the `[container]` macro auto-emits a `WindowState` global for. + +**4. Widgets — caller-owned globals (preferred for ports).** Use the `[edit_widget]` rail with `safe_addr(global)`. No state-struct migration of your data. + +| v1 raw | boost-v2 wrapper | +|---|---| +| `Checkbox("L", unsafe(addr(b)))` | `edit_checkbox(safe_addr(b), (id = "K", text = "L"))` | +| `InputFloat("L", unsafe(addr(f)), step)` | `edit_input_float(safe_addr(f), (id = "K", text = "L", step = step))` | +| Adjacent `InputFloat(field.x)` + `InputFloat(field.y)` | Collapse → `edit_input_float2(safe_addr(field), (id = "K", text = "L"))` (same for float3/float4) | + +`id = "..."` is a required string-literal at every `[edit_widget]` call site (telemetry path key). + +For struct-field access (no `safe_addr` allowed): `unsafe(addr(struct.field))`. But the cleaner move is to collapse adjacent field edits into a vector widget. + +**5. Narrative widgets.** `Text("...")` → `text("...")` — Form 3 positional-string sugar; macro auto-generates the identifier from line info. `Separator()` → `separator(SEP_IDENT)` (no string-sugar, needs explicit ident — `[widget]` macro spelling). + +**6. Drop `FontGlobalScale` / `StyleColorsDark` calls** — theme picks 14px JetBrains Mono. For a bigger font, call `load_daslang_font(18.0f)` BEFORE `harness_init`, not `FontGlobalScale`. + +**7. Custom GL + ImGui hybrid.** See split-harness recipe in `imgui-harness-split-custom-opengl-overlay-recipe`. Summary: `_allow_glfw_calls = true` + explicit requires for `live/glfw_live` + `imgui_app` + `glfw/glfw_boost` + `opengl/opengl_boost`, replace `harness_end_frame` with manual `end_of_frame` / `Render` / `ImGui_ImplOpenGL3_RenderDrawData` / `live_end_frame`. + +**Widget-rail decision (when both work):** +- `[edit_widget]` rail (`edit_*` family) — caller-owned `T?`, `{value}` payload, `id=` opt-in. Best for ports (your existing `var foo : T` stays). +- `[widget]` rail (`checkbox`/`input_float`/etc.) — auto-emitted state struct (`ToggleState`/`InputStateFloat`/etc.), full state surface (pending, changed, click count). Best for greenfield where state IS the data. + +## Questions +- What's the conversion checklist for porting a v1 dasImgui imgui_boost example to the boost-v2 harness + widget surface? diff --git a/mouse-data/docs/what-s-the-columns-open-boost-surface-in-dasimgui-does-it-support-dynamic-id-and-border-for-runtime-shape-columns-n-id-border-co.md b/mouse-data/docs/what-s-the-columns-open-boost-surface-in-dasimgui-does-it-support-dynamic-id-and-border-for-runtime-shape-columns-n-id-border-co.md new file mode 100644 index 000000000..c9859b0b7 --- /dev/null +++ b/mouse-data/docs/what-s-the-columns-open-boost-surface-in-dasimgui-does-it-support-dynamic-id-and-border-for-runtime-shape-columns-n-id-border-co.md @@ -0,0 +1,38 @@ +--- +slug: what-s-the-columns-open-boost-surface-in-dasimgui-does-it-support-dynamic-id-and-border-for-runtime-shape-columns-n-id-border-co +title: What's the columns_open boost surface in dasImgui — does it support dynamic id and border for runtime-shape Columns(n, id, border) consumers? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +As of 2026-05-16 (PR #40), `columns_open(n; id : string = ""; border : bool = true)` accepts both id (id-stack tag for column-width persistence) and border (vertical-divider toggle) as defaulted trailing params. Use the new signature when a `Columns(n, id, border)` consumer can't go through the static `columns(N, ${col0}, …, ${colN-1})` call_macro — dynamic column count, loop-driven shape, etc. + +```daslang +require imgui/imgui_layout_builtin + +// Static shape — use the columns(N, …) call_macro: +columns(3, ${col0}, ${col1}, ${col2}) + +// Dynamic shape — call the primitive trio directly: +columns_open(COL_COUNT, "table-id", BORDER_FLAG) +for (i in range(COL_COUNT * rows)) { + text("cell {i}") + next_col() +} +columns_close() +``` + +**Backwards-compat:** the older `columns_open(n)` call (used by the `columns(N, …)` macro's expansion) still works — `id=""` and `border=true` are the defaults. The call_macro doesn't break. + +**Three sub-section patterns from `examples/imgui_demo/columns.das`:** +1. Loop-over-N items with no border: `columns_open(3, "mycolumns3", false)` — Basic 14-item loop. +2. Dynamic count + dynamic border: `columns_open(COLUMNS_BORDERS_COUNT, "", COLUMNS_BORDERS_V)` — Borders demo with user-driven count. +3. Custom id, default border: `columns_open(2, "tree")` — Tree demo (relies on `border=true` default). + +**Layout primitives sibling rail:** the same module provides `next_col()`, `columns_close()`, and `list_clipper(items_count, body)`. All three are public exports of `imgui_layout_builtin`. + +**Reference:** `widgets/imgui_layout_builtin.das:columns_open` (and the `columns(N, …)` ColumnsCallMacro just below). + +## Questions +- What's the columns_open boost surface in dasImgui — does it support dynamic id and border for runtime-shape Columns(n, id, border) consumers? diff --git a/mouse-data/docs/what-s-the-dasimgui-boost-wrapper-for-imguilistclipper-how-do-i-cull-a-virtual-list-to-the-visible-region-without-inline-begin-s.md b/mouse-data/docs/what-s-the-dasimgui-boost-wrapper-for-imguilistclipper-how-do-i-cull-a-virtual-list-to-the-visible-region-without-inline-begin-s.md new file mode 100644 index 000000000..9bac78816 --- /dev/null +++ b/mouse-data/docs/what-s-the-dasimgui-boost-wrapper-for-imguilistclipper-how-do-i-cull-a-virtual-list-to-the-visible-region-without-inline-begin-s.md @@ -0,0 +1,42 @@ +--- +slug: what-s-the-dasimgui-boost-wrapper-for-imguilistclipper-how-do-i-cull-a-virtual-list-to-the-visible-region-without-inline-begin-s +title: What's the dasImgui boost wrapper for ImGuiListClipper — how do I cull a virtual list to the visible region without inline Begin/Step/End? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +`list_clipper(items_count, body)` in `widgets/imgui_layout_builtin.das` (added 2026-05-16, PR #40). Takes a `block<(start : int; end_ : int) : void>` callback that fires once per visible chunk with the `[start, end_)` index range. + +```daslang +require imgui/imgui_layout_builtin // brings list_clipper + +child(LOG_SCROLL_CHILD, (text = "scrolling", size = float2(0.0f, 0.0f), …)) { + list_clipper(length(LOG_ITEMS)) <| $(start, end_) { + for (i in range(start, end_)) { + text(LOG_ITEM_TEXT[i], (text = LOG_ITEMS[i])) + } + } +} +``` + +The wrapper internally constructs `ImGuiListClipper`, calls Begin/Step/End inside an `unsafe { ... }` block. Replaces the four-line cargo at every consumer: + +```daslang +// OLD — inline: +unsafe { + var clipper = ImGuiListClipper() + clipper |> Begin(2000, -1.0f) + while (clipper |> Step()) { + for (i in range(clipper.DisplayStart, clipper.DisplayEnd)) { … } + } + clipper |> End() +} +``` + +**When NOT to use:** if the visible range isn't random-access by physical row (e.g., filtered log lines where filter rejection breaks the indexed mapping), drop to a plain `for (i in range(length(items)))` since the clipper assumes O(1) random-access to row N. The `app_log.das` port uses both paths: clipper on the unfiltered render, plain `for` on the filtered render. + +**Reference:** `widgets/imgui_layout_builtin.das:list_clipper`, used in `examples/imgui_demo/columns.das` (Horizontal Scrolling section) and `examples/imgui_demo/app_log.das`. + +## Questions +- What's the dasImgui boost wrapper for ImGuiListClipper — how do I cull a virtual list to the visible region without inline Begin/Step/End? diff --git a/mouse-data/docs/when-detecting-whether-a-dasimgui-text-filter-is-active-why-is-passes-filter-state-wrong-and-what-s-the-right-helper.md b/mouse-data/docs/when-detecting-whether-a-dasimgui-text-filter-is-active-why-is-passes-filter-state-wrong-and-what-s-the-right-helper.md new file mode 100644 index 000000000..98b81277c --- /dev/null +++ b/mouse-data/docs/when-detecting-whether-a-dasimgui-text-filter-is-active-why-is-passes-filter-state-wrong-and-what-s-the-right-helper.md @@ -0,0 +1,54 @@ +--- +slug: when-detecting-whether-a-dasimgui-text-filter-is-active-why-is-passes-filter-state-wrong-and-what-s-the-right-helper +title: When detecting whether a dasImgui text_filter is active, why is `passes_filter(state, "")` wrong and what's the right helper? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +**TL;DR — use `is_active(state)`, not `passes_filter(state, "")`.** + +Wrong: +```daslang +if (passes_filter(LOG_FILTER, "")) { + // assume filter inactive +} +``` + +Right: +```daslang +if (!is_active(LOG_FILTER)) { + // filter inactive +} +``` + +## Why `passes_filter(state, "")` is broken + +`passes_filter` bottoms out to `ImGuiTextFilter::PassFilter(text, nullptr)`. Per the C++ semantics: + +- Empty filter (no terms) ⇒ `PassFilter(anything)` returns `true`. +- Filter with INCLUDE terms only (e.g. `info,warn`) ⇒ `PassFilter("")` returns `false` (empty string contains no include term). +- Filter with EXCLUDE terms only (e.g. `-error`) ⇒ `PassFilter("")` returns `true` (empty string isn't `-error`-matched). +- Mixed include + exclude ⇒ exclude-first short-circuit, then include check. + +The "exclude-only" case is the trap. `passes_filter(state, "")` returns `true` for both "filter inactive" AND "filter has only exclude terms". Code that uses this as the inactivity gate (e.g. to skip filter checks during clipper-cull) will render lines the user explicitly excluded. + +## The right primitive + +`def public is_active(var state : TextFilterState) : bool` in `widgets/imgui_widgets_builtin.das` (added in PR #40, 2026-05-16). Delegates directly to `ImGuiTextFilter::IsActive()`: + +```cpp +bool ImGuiTextFilter::IsActive() const { return !Filters.empty(); } +``` + +True iff parsing produced ≥1 include OR exclude term — exactly what "is the filter active" means in C++. + +## Reference + +- Caught by Copilot review on PR #40 round 1 ([discussion](https://github.com/borisbat/dasImgui/pull/40#discussion_r3253962411)); fix in `f020f25`. +- Helper definition: `widgets/imgui_widgets_builtin.das:is_active`. +- `passes_filter` docstring carries an inline warning against using it as an active-state proxy. +- Original pattern lives in `examples/imgui_demo/app_log.das` ShowExampleAppLog scroll-region clipper gate. Same C++ idiom is at `imgui_demo.cpp:7554` (`if (Filter.IsActive()) { …non-clipper… } else { …clipper… }`). + +## Questions +- When detecting whether a dasImgui text_filter is active, why is `passes_filter(state, "")` wrong and what's the right helper? diff --git a/mouse-data/docs/why-are-imgui-main-menu-bar-child-menu-bboxes-0-0-0-0-in-imgui-snapshot-how-do-i-click-a-file-edit-menu-header-from-a-record-das.md b/mouse-data/docs/why-are-imgui-main-menu-bar-child-menu-bboxes-0-0-0-0-in-imgui-snapshot-how-do-i-click-a-file-edit-menu-header-from-a-record-das.md new file mode 100644 index 000000000..941b026eb --- /dev/null +++ b/mouse-data/docs/why-are-imgui-main-menu-bar-child-menu-bboxes-0-0-0-0-in-imgui-snapshot-how-do-i-click-a-file-edit-menu-header-from-a-record-das.md @@ -0,0 +1,74 @@ +--- +slug: why-are-imgui-main-menu-bar-child-menu-bboxes-0-0-0-0-in-imgui-snapshot-how-do-i-click-a-file-edit-menu-header-from-a-record-das +title: why are imgui main_menu_bar child menu() bboxes (0,0,0,0) in imgui_snapshot — how do I click a File/Edit menu header from a record_*.das driver? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +# `menu()` headers register bbox correctly; only `main_menu_bar()` root is degenerate + +**Updated 2026-05-17:** `widgets/imgui_containers_builtin.das` now snapshots +`GetItemRectMin/Max` right after `BeginMenu` / `BeginTabItem` returns, while +the parent-strip header is still the "last item." So: + +- `menu()` children of `main_menu_bar` / `menu_bar` — bbox = the clickable + header rect in the parent strip. `widget_center(snap, "MAIN_BAR/APP_MENU_FILE")` + returns the visual center of the "File" label. +- `tab_item()` headers — same fix; bbox = the clickable tab strip rect. +- `main_menu_bar()` ROOT — STILL `(0,0,0,0)`. The bar chrome has no + meaningful "header" — there's no parent strip to click into. If you need + to click outside any open child menu to dismiss it, hardcode pixel coords. + +Confirmed via snapshot diff against the fix commit (style_editor's +`STYLE_TAB_COLORS` registered `bbox = (119,168,171,188)` post-fix vs +`(0,0,0,0)` pre-fix). + +## How to click a menu header now + + var snap = wait_for_render(app, "MAIN_BAR_DEMO/APP_MENU_FILE", 10.0f) + let p_file = widget_center(snap, "MAIN_BAR_DEMO/APP_MENU_FILE") + var events : array + events |> click_at(0, p_file, 250) + post_command(app, "imgui_mouse_play", JV((events = events))) + +For submenu items: items DO register bboxes once the parent menu is open. +Click the parent first (via its header bbox), `wait_for_render` for the +child path, then `widget_center` the child. + +## Historical artifact + +`tests/integration/record_imgui_demo_app_main_menu.das` predates this fix +and bakes empirical pixel coords: + + let P_FILE_HEADER = (24.0f, 10.0f) + let P_EDIT_HEADER = (56.0f, 10.0f) + let P_FILE_COLORS = (50.0f, 173.0f) + +Already-committed APNG works; not worth re-recording. **New drivers +should use `widget_center`** for menu children and tab items. + +## Root cause that got fixed + +`stateless_finalize` and `open_close_finalize` previously ignored the +item-rect entirely (default-constructed entry → zero bbox). The fix +takes an optional `bbox : float4` parameter and stamps it before +`container_finalize` writes the registry entry. `menu()` and +`tab_item()` capture via a new `current_item_bbox()` helper between +their `BeginXxx()` and `invoke(blk)` calls. + +`widgets/imgui_containers_builtin.das:43-89` for the helper signatures; +`menu()` at line ~205 and `tab_item()` at line ~377 for the call sites. + +## Related + +- `examples/imgui_demo/app_main_menu.das` — main menu bar port. +- `examples/imgui_demo/style_editor.das` — exercises `tab_item` bbox. +- `tests/integration/record_imgui_demo_style_editor.das` — first + recording driver to rely on the bbox fix (clicks 4 tabs via + `widget_center`). + +## Questions +- why are imgui main_menu_bar child menu() bboxes (0,0,0,0) in imgui_snapshot — how do I click a File/Edit menu header from a record_*.das driver? +- can I use widget_center on a menu() child or tab_item() in dasImgui? +- after the 2026-05-17 fix, which containers still have bbox=(0,0,0,0)? diff --git a/site/_news/2026-05-17-daspkg-is-live.md b/site/_news/2026-05-17-daspkg-is-live.md new file mode 100644 index 000000000..f8cad9f36 --- /dev/null +++ b/site/_news/2026-05-17-daspkg-is-live.md @@ -0,0 +1,6 @@ +--- +date: 2026-05-17 +tag: daspkg +title: daspkg is live — the daslang package index. Browse bindings, bots, batteries — one install away. +link: /daspkg.html +--- diff --git a/site/blog/template.html b/site/blog/template.html index 357f933b3..fe770b332 100644 --- a/site/blog/template.html +++ b/site/blog/template.html @@ -33,6 +33,7 @@ docs benchmarks downloads + daspkg blog community diff --git a/site/daspkg.html b/site/daspkg.html new file mode 100644 index 000000000..238640b15 --- /dev/null +++ b/site/daspkg.html @@ -0,0 +1,158 @@ + + + + + + daspkg — Daslang package manager + + + + + + + + + + + + + + +
+ + + + + +
+
+
+
▮ daspkg · package index
+

+ Bindings, bots, batteries. + One install away. +

+

+ daspkg is the daslang package manager. + The list below is rendered straight from + github.com/borisbat/daspkg-index + — adding a package is a single CLI command. No accounts, no web upload. +

+
+
+ $ + daspkg install + ‹package› + +
+ cli docs → +
+
+
+
publish a package
+
+
$ daspkg introduce github.com/you/repo
+
# validates your .das_package manifest
+
# opens a PR against the index repo
+
+
+ loading packages… + full CLI reference → +
+
+
+
+ + +
+
+
+ +
+
+ / +
+
+
+
+ + +
+
+
+
loading packages…
+
+
+ + + + +
+ + + diff --git a/site/downloads.html b/site/downloads.html index cf485f5b2..3951d6e68 100644 --- a/site/downloads.html +++ b/site/downloads.html @@ -28,6 +28,7 @@ docs benchmarks downloads + daspkg blog community playground diff --git a/site/files/daspkg.js b/site/files/daspkg.js new file mode 100644 index 000000000..9bae73501 --- /dev/null +++ b/site/files/daspkg.js @@ -0,0 +1,200 @@ +/* daspkg.js — fetch the live package index, render filterable card grid. + * Source of truth: github.com/borisbat/daspkg-index/packages.json. */ +(function () { + 'use strict'; + + var INDEX_URL = 'https://raw.githubusercontent.com/borisbat/daspkg-index/main/packages.json'; + + var els = { + list: document.getElementById('daspkg-list'), + status: document.getElementById('daspkg-status'), + tags: document.getElementById('daspkg-tags'), + search: document.getElementById('daspkg-search'), + searchClear: document.getElementById('daspkg-search-clear'), + countN: document.getElementById('daspkg-count-n'), + countTotal: document.getElementById('daspkg-count-total'), + publishCount: document.getElementById('daspkg-publish-count'), + }; + + var state = { + all: [], + allTags: [], + query: '', + activeTags: [], + }; + + function esc(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function applyFilter() { + var q = state.query.trim().toLowerCase(); + return state.all.filter(function (p) { + if (state.activeTags.length) { + var tags = p.tags || []; + for (var i = 0; i < state.activeTags.length; i++) { + if (tags.indexOf(state.activeTags[i]) === -1) return false; + } + } + if (!q) return true; + if (p.name && p.name.toLowerCase().indexOf(q) !== -1) return true; + if (p.description && p.description.toLowerCase().indexOf(q) !== -1) return true; + if (p.author && p.author.toLowerCase().indexOf(q) !== -1) return true; + var ts = p.tags || []; + for (var j = 0; j < ts.length; j++) if (ts[j].toLowerCase().indexOf(q) !== -1) return true; + return false; + }); + } + + function renderTags() { + els.tags.innerHTML = state.allTags.map(function (t) { + var active = state.activeTags.indexOf(t) !== -1; + return ''; + }).join(''); + } + + function renderCards(pkgs) { + if (!pkgs.length) { + els.list.innerHTML = ''; + els.status.textContent = 'no packages match the filter'; + els.status.hidden = false; + return; + } + els.status.hidden = true; + els.list.innerHTML = pkgs.map(function (p) { + var native = p.has_native + ? 'native' : ''; + var sdk = p.min_sdk + ? 'sdk ≥ ' + esc(p.min_sdk) + '' : ''; + var license = p.license + ? '' + esc(p.license) + '' : ''; + var tags = (p.tags || []).map(function (t) { + return '' + esc(t) + ''; + }).join(''); + var deps = (p.dependencies && p.dependencies.length) + ? '
↳ depends on ' + + p.dependencies.map(esc).join(', ') + '
' : ''; + var url = p.url ? 'https://' + p.url : '#'; + var installStr = 'daspkg install ' + p.name; + return '' + + '' + + '
' + + '
' + + '

' + esc(p.name) + '

' + native + sdk + + '
' + license + + '
' + + '

' + esc(p.description || '') + '

' + + '
' + tags + '
' + + deps + + '
' + + '
' + + '$' + + '' + esc(installStr) + '' + + '' + + '
' + + 'repo ↗' + + '
' + + '
'; + }).join(''); + } + + function updateCounts(visible) { + els.countN.textContent = visible; + els.countTotal.textContent = state.all.length; + els.publishCount.textContent = visible + ' of ' + state.all.length + ' packages'; + } + + function rerender() { + var pkgs = applyFilter(); + renderTags(); + renderCards(pkgs); + updateCounts(pkgs.length); + } + + function copyToClipboard(text, srcEl) { + var done = function (ok) { + if (!srcEl) return; + var prev = srcEl.textContent; + srcEl.textContent = ok ? '✓' : '⎘'; + setTimeout(function () { srcEl.textContent = prev; }, 900); + }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { done(true); }, function () { done(false); }); + } else { + try { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = document.execCommand('copy'); + document.body.removeChild(ta); + done(ok); + } catch (e) { done(false); } + } + } + + function wireEvents() { + els.tags.addEventListener('click', function (e) { + var btn = e.target.closest('[data-tag]'); + if (!btn) return; + var tag = btn.getAttribute('data-tag'); + var i = state.activeTags.indexOf(tag); + if (i === -1) state.activeTags.push(tag); + else state.activeTags.splice(i, 1); + rerender(); + }); + els.search.addEventListener('input', function () { + state.query = els.search.value; + els.searchClear.hidden = !state.query; + rerender(); + }); + els.searchClear.addEventListener('click', function () { + els.search.value = ''; + state.query = ''; + els.searchClear.hidden = true; + rerender(); + els.search.focus(); + }); + document.addEventListener('click', function (e) { + var copy = e.target.closest('[data-copy]'); + if (!copy) return; + e.preventDefault(); + e.stopPropagation(); + copyToClipboard(copy.getAttribute('data-copy'), copy); + }); + } + + function init(packages) { + state.all = packages.slice(); + var tagSet = {}; + state.all.forEach(function (p) { + (p.tags || []).forEach(function (t) { tagSet[t] = true; }); + }); + state.allTags = Object.keys(tagSet).sort(); + wireEvents(); + rerender(); + } + + function fail(err) { + els.status.textContent = 'failed to load package index — ' + (err && err.message ? err.message : 'network error'); + els.publishCount.textContent = '— packages'; + els.countN.textContent = '0'; + els.countTotal.textContent = '0'; + } + + fetch(INDEX_URL, { cache: 'no-cache' }) + .then(function (r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(init) + .catch(fail); +}()); diff --git a/site/files/forge.css b/site/files/forge.css index 7b01f331c..9c6410029 100644 --- a/site/files/forge.css +++ b/site/files/forge.css @@ -113,6 +113,7 @@ button { font: inherit; border: 0; background: none; color: inherit; cursor: poi font-size: 12.5px; color: var(--fg-dim); } +.forge-nav__links a.is-active { color: var(--amber); } .forge-nav__right { display: flex; align-items: center; @@ -987,3 +988,500 @@ button { font: inherit; border: 0; background: none; color: inherit; cursor: poi .forge-features__grid { grid-template-columns: repeat(2, 1fr); } .forge-footer__grid { grid-template-columns: 1fr 1fr 1fr; } } + +/* ───────────────────────────────────────────────────────────── + daspkg — package index page (/daspkg.html) + landing callout + ───────────────────────────────────────────────────────────── */ + +/* Inline-code chip used in hero copy + landing callout. */ +.forge-inline-code { + font-family: var(--font-mono); + font-size: 13px; + padding: 1px 6px; + background: var(--bg-2); + border: 1px solid var(--rule); + border-radius: 3px; + color: var(--amber); +} + +/* ───── Hero (/daspkg.html) ───── */ + +.forge-daspkg-hero { + position: relative; + overflow: hidden; + padding: 52px 0 40px; + border-bottom: 1px solid var(--rule); +} +.forge-daspkg-hero::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background-image: + radial-gradient(700px 280px at 85% -60px, rgba(232, 161, 58, 0.10), transparent 60%), + radial-gradient(circle at center, rgba(255, 255, 255, 0.04) 1px, transparent 1px); + background-size: auto, 22px 22px; + -webkit-mask-image: linear-gradient(to bottom, black, transparent); + mask-image: linear-gradient(to bottom, black, transparent); +} +.forge-daspkg-hero__grid { + position: relative; + display: grid; + grid-template-columns: 1.05fr 1fr; + gap: 48px; + align-items: end; +} +.forge-daspkg-hero h1 { + font-family: var(--font-sans); + font-size: 56px; + line-height: 1.02; + font-weight: 600; + letter-spacing: -0.03em; + color: var(--fg); + margin: 0; +} +.forge-daspkg-hero h1 em { + font-style: italic; + font-weight: 400; + color: var(--amber); +} +.forge-daspkg-hero__kicker { + font-family: var(--font-mono); + font-size: 12px; + color: var(--amber); + letter-spacing: 1.4px; + text-transform: uppercase; + margin-bottom: 14px; +} +.forge-daspkg-hero__lede { + margin-top: 18px; + font-size: 16px; + line-height: 1.6; + color: var(--fg-dim); + max-width: 560px; +} +.forge-daspkg-hero__lede a { + color: var(--amber); + border-bottom: 1px solid var(--amber-dim); +} +.forge-daspkg-hero__ctas { + margin-top: 24px; + display: flex; + gap: 18px; + align-items: center; + flex-wrap: wrap; +} +.forge-daspkg-cmd { + font-family: var(--font-mono); + font-size: 13px; + background: var(--bg-2); + border: 1px solid var(--rule); + padding: 8px 12px; + border-radius: 5px; + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--fg); +} +.forge-daspkg-cmd .prompt { color: var(--green); } +.forge-daspkg-cmd .placeholder { color: var(--fg-dim); } +.forge-daspkg-cmd .copy { + margin-left: 6px; + color: var(--fg-faint); + cursor: copy; + user-select: none; +} +.forge-daspkg-cmd .copy:hover { color: var(--amber); } +.forge-daspkg-hero__doc-link { + font-family: var(--font-mono); + font-size: 12.5px; + color: var(--fg-dim); + border-bottom: 1px solid var(--rule); +} + +/* "publish a package" card (right column of the hero) */ +.forge-daspkg-publish { + background: var(--bg-2); + border: 1px solid var(--rule); + border-radius: 6px; + padding: 18px 20px; +} +.forge-daspkg-publish__head { + font-family: var(--font-mono); + font-size: 11px; + color: var(--fg-faint); + letter-spacing: 1.3px; + text-transform: uppercase; + margin-bottom: 14px; +} +.forge-daspkg-publish__body { + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.7; + color: var(--fg); +} +.forge-daspkg-publish__body .prompt { color: var(--green); } +.forge-daspkg-publish__body .placeholder { color: var(--fg-dim); } +.forge-daspkg-publish__body .comment { color: var(--fg-faint); } +.forge-daspkg-publish__foot { + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid var(--rule); + display: flex; + justify-content: space-between; + font-family: var(--font-mono); + font-size: 11.5px; +} +.forge-daspkg-publish__count { color: var(--fg-faint); } +.forge-daspkg-publish__cli { color: var(--amber); } + +/* ───── Filter strip ───── */ + +.forge-daspkg-filter { + border-bottom: 1px solid var(--rule); + padding: 28px 0 22px; +} +.forge-daspkg-filter__row { + display: grid; + grid-template-columns: 320px 1fr auto; + gap: 24px; + align-items: center; +} +.forge-daspkg-search { + display: flex; + align-items: center; + gap: 10px; + background: var(--bg-2); + border: 1px solid var(--rule); + border-radius: 5px; + padding: 8px 12px; +} +.forge-daspkg-search__glyph { + color: var(--fg-faint); + font-family: var(--font-mono); + font-size: 13px; +} +.forge-daspkg-search input { + flex: 1; + background: transparent; + border: 0; + outline: 0; + color: var(--fg); + font-family: var(--font-mono); + font-size: 13px; +} +.forge-daspkg-search input::placeholder { color: var(--fg-faint); } +.forge-daspkg-search__clear { + color: var(--fg-faint); + font-family: var(--font-mono); + font-size: 12px; + cursor: pointer; + background: none; + border: 0; + padding: 0; +} +.forge-daspkg-search__clear:hover { color: var(--amber); } +.forge-daspkg-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.forge-daspkg-chip { + font-family: var(--font-mono); + font-size: 11.5px; + padding: 4px 9px; + border-radius: 3px; + cursor: pointer; + background: var(--bg-2); + color: var(--fg-dim); + border: 1px solid var(--rule); + font-weight: 400; + user-select: none; + transition: background 80ms ease, color 80ms ease, border-color 80ms ease; +} +.forge-daspkg-chip:hover { border-color: var(--amber-dim); color: var(--fg); } +.forge-daspkg-chip.is-active { + background: var(--amber); + color: var(--bg); + border-color: var(--amber); + font-weight: 600; +} +.forge-daspkg-count { + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--fg-faint); + white-space: nowrap; +} +.forge-daspkg-count__n { color: var(--fg); } + +/* ───── Grid + cards ───── */ + +.forge-daspkg-grid { + padding: 36px 0 80px; +} +.forge-daspkg-grid__list { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} +.forge-daspkg-empty { + padding: 60px 0; + text-align: center; + color: var(--fg-faint); + font-family: var(--font-mono); + font-size: 13px; +} +.forge-daspkg-card { + background: var(--bg-2); + border: 1px solid var(--rule); + border-radius: 6px; + padding: 20px 22px 18px; + display: flex; + flex-direction: column; + gap: 14px; + cursor: pointer; + transition: border-color 120ms ease; + color: inherit; + text-decoration: none; +} +.forge-daspkg-card:hover { border-color: var(--amber-dim); } +.forge-daspkg-card:hover .forge-daspkg-card__repo { color: var(--amber); } +.forge-daspkg-card__head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; +} +.forge-daspkg-card__title-row { + display: flex; + align-items: baseline; + gap: 10px; + flex-wrap: wrap; +} +.forge-daspkg-card h3 { + margin: 0; + font-family: var(--font-mono); + font-size: 17px; + font-weight: 600; + color: var(--fg); + letter-spacing: -0.005em; +} +.forge-daspkg-card__native { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--amber); + border: 1px solid var(--amber-dim); + padding: 1px 6px; + border-radius: 2px; + letter-spacing: 0.6px; +} +.forge-daspkg-card__sdk { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--fg-faint); +} +.forge-daspkg-card__license { + font-family: var(--font-mono); + font-size: 11px; + color: var(--fg-faint); +} +.forge-daspkg-card__desc { + margin: 0; + color: var(--fg-dim); + font-size: 14px; + line-height: 1.55; +} +.forge-daspkg-card__tags { + display: flex; + flex-wrap: wrap; + gap: 5px; +} +.forge-daspkg-card__tag { + font-family: var(--font-mono); + font-size: 10.5px; + padding: 2px 6px; + border-radius: 2px; + color: var(--fg-dim); + background: var(--bg-2); + border: 1px solid var(--rule); +} +.forge-daspkg-card__deps { + font-family: var(--font-mono); + font-size: 11px; + color: var(--fg-faint); + margin-top: -6px; +} +.forge-daspkg-card__foot { + margin-top: auto; + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 12px; + border-top: 1px solid var(--rule); +} +.forge-daspkg-card__install { + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--fg-dim); + display: flex; + align-items: center; + gap: 8px; +} +.forge-daspkg-card__install .prompt { color: var(--green); } +.forge-daspkg-card__install .copy { + color: var(--fg-faint); + cursor: copy; + user-select: none; +} +.forge-daspkg-card__install .copy:hover { color: var(--amber); } +.forge-daspkg-card__repo { + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--fg-dim); + transition: color 120ms ease; +} + +/* ───── Landing-page callout (§ 05 packages) ───── */ + +.forge-pkg-callout__grid { + display: grid; + grid-template-columns: 1fr 1.3fr; + gap: 56px; + align-items: start; +} +.forge-pkg-callout h2 { + font-family: var(--font-sans); + font-size: 38px; + font-weight: 600; + letter-spacing: -0.022em; + line-height: 1.1; + margin: 0; + color: var(--fg); +} +.forge-pkg-callout h2 em { + font-style: italic; + font-weight: 400; + color: var(--amber); +} +.forge-pkg-callout__lede { + color: var(--fg-dim); + font-size: 16px; + line-height: 1.6; + margin-top: 18px; + max-width: 460px; +} +.forge-pkg-callout__ctas { + margin-top: 22px; + display: flex; + gap: 12px; + flex-wrap: wrap; +} +.forge-pkg-callout__primary { + background: var(--amber); + color: var(--bg); + font-family: var(--font-mono); + font-weight: 600; + padding: 10px 16px; + border-radius: 5px; + font-size: 13px; + text-decoration: none; +} +.forge-pkg-callout__primary:hover { color: var(--bg); background: #f4b04a; } +.forge-pkg-callout__secondary { + color: var(--fg); + font-family: var(--font-mono); + padding: 10px 16px; + font-size: 13px; + border: 1px solid var(--rule); + border-radius: 5px; + text-decoration: none; +} +.forge-pkg-callout__secondary:hover { border-color: var(--amber-dim); } +.forge-pkg-callout__index { + margin-top: 20px; + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--fg-faint); +} +.forge-pkg-callout__term { + background: var(--bg-2); + border: 1px solid var(--rule); + border-radius: 6px; + padding: 14px 16px; + font-family: var(--font-mono); + font-size: 13px; + color: var(--fg); + line-height: 1.6; +} +.forge-pkg-callout__term .prompt { color: var(--green); } +.forge-pkg-callout__term .ok { color: var(--green); } +.forge-pkg-callout__term .dim { color: var(--fg-faint); } +.forge-pkg-callout__cells { + margin-top: 18px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1px; + background: var(--rule); + border: 1px solid var(--rule); + border-radius: 6px; + overflow: hidden; +} +.forge-pkg-callout__cell { + background: var(--bg); + padding: 16px 16px 18px; + text-decoration: none; + color: inherit; + display: block; + transition: background 120ms ease; +} +.forge-pkg-callout__cell:hover { background: var(--bg-3); } +.forge-pkg-callout__cell:hover .forge-pkg-callout__cell-name { color: var(--amber); } +.forge-pkg-callout__cell-name { + font-family: var(--font-mono); + font-size: 13px; + font-weight: 600; + color: var(--fg); + transition: color 120ms ease; +} +.forge-pkg-callout__cell-tags { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--fg-faint); + margin-top: 4px; +} +.forge-pkg-callout__cell-desc { + color: var(--fg-dim); + font-size: 12.5px; + line-height: 1.5; + margin-top: 10px; +} +.forge-pkg-callout__indexed { + margin-top: 12px; + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--fg-faint); + text-align: right; +} + +/* ───── Responsive ───── */ + +@media (max-width: 1023px) { + .forge-daspkg-hero__grid { grid-template-columns: 1fr; gap: 32px; align-items: start; } + .forge-pkg-callout__grid { grid-template-columns: 1fr; gap: 36px; } +} + +@media (max-width: 767px) { + .forge-daspkg-hero { padding: 36px 0 28px; } + .forge-daspkg-hero h1 { font-size: 38px; } + .forge-daspkg-hero__ctas { flex-direction: column; align-items: stretch; } + .forge-daspkg-cmd { width: 100%; } + .forge-daspkg-filter__row { + grid-template-columns: 1fr; + gap: 14px; + } + .forge-daspkg-search { width: 100%; } + .forge-daspkg-grid__list { grid-template-columns: 1fr; } + .forge-daspkg-grid { padding: 28px 0 60px; } + .forge-pkg-callout h2 { font-size: 26px; } + .forge-pkg-callout__cells { grid-template-columns: 1fr; } +} diff --git a/site/index.html b/site/index.html index 9168f991a..f54171ea5 100644 --- a/site/index.html +++ b/site/index.html @@ -43,6 +43,7 @@ docs benchmarks downloads + daspkg blog community playground @@ -305,12 +306,72 @@

Grab a release. Or build from source.

- -
+ +
+
+
+

+ A package manager that + compiles in. +

+

+ daspkg is the daslang package + manager. Bindings for Dear ImGui, the Claude API, Telegram bots — + one command, no accounts. Publishing is a single CLI call against + an open index repo. +

+ + +
+ +
+
+
+ + +
+
+
diff --git a/site/playground/index.html b/site/playground/index.html index 088e12dba..8554b6d61 100644 --- a/site/playground/index.html +++ b/site/playground/index.html @@ -54,6 +54,7 @@ docs benchmarks downloads + daspkg blog community
diff --git a/site/playground/placeholder.html b/site/playground/placeholder.html index 405fa6b6e..ec83fe63b 100644 --- a/site/playground/placeholder.html +++ b/site/playground/placeholder.html @@ -27,6 +27,7 @@ docs benchmarks downloads + daspkg blog community