Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions modules/dasGlfw/dasglfw/glfw_live.das
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,16 +33,45 @@ 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) {
panic("glfw_live: can't init glfw")
}
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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <dasImgui_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?
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions mouse-data/docs/imgui-harness-lint-forbidden-modules-harness001.md
Original file line number Diff line number Diff line change
@@ -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?
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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<JsonValue?>
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)?
Loading
Loading