Skip to content
Closed
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
14 changes: 13 additions & 1 deletion tests/integration/test_imgui_synth_ctrl_shortcuts.das
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,19 @@ def test_imgui_synth_ctrl_shortcuts(t : T?) {
let v = snap?["globals"]?["MAIN_WIN/NAME_INPUT"]?["payload"]?["value"] ?? ""
return v == "abc"
}
t |> success(typed != null, "NAME_INPUT.value == \"abc\" before chord")
// [PROBE — remove after diagnosis] capture the actual state when this fails on CI:
// disambiguates activation-race (active_widget != NAME_INPUT) vs keys-lost (val="",
// timeline drained q=N/N) vs insert-not-replace (val="Name...") vs stall (key_playing,
// q stuck at 0).
let dbg_snap = snapshot(d)
let dbg_val = dbg_snap?["globals"]?["MAIN_WIN/NAME_INPUT"]?["payload"]?["value"] ?? "?"
let dbg_aw = dbg_snap?["io"]?["active_widget"] ?? "none"
let dbg_focus = dbg_snap?["globals"]?["MAIN_WIN/NAME_INPUT"]?["focus"] ?? false
let dbg_ks = post_command(d, "imgui_key_status", null)
let dbg_kp = dbg_ks?["playing"] ?? true
let dbg_qi = dbg_ks?["queue_idx"] ?? -1
let dbg_qt = dbg_ks?["queue_total"] ?? -1
t |> success(typed != null, "NAME_INPUT.value == \"abc\" before chord [val='{dbg_val}' active_widget={dbg_aw} focus={dbg_focus} key_playing={dbg_kp} q={dbg_qi}/{dbg_qt}]")

// The value showing "abc" proves the chars landed, but the key_type
// timeline's final per-char delay may still be ticking; let it reach
Expand Down
34 changes: 29 additions & 5 deletions widgets/imgui_live_core.das
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,20 @@ var private mouse_playing : bool = false
var private last_move_x : float = 0.0
var private last_move_y : float = 0.0
var private last_move_t : int = -1
// Frame-paced playback: when set, advance steps one t_ms-group per FRAME (t_ms read as
// a frame index) instead of by wall-clock — so a drag delivers a per-frame cursor delta
// regardless of frame rate. A wall-clock drag collapses press+move into a single frame
// on a slow/stalled frame (the CI runner), landing a zero-delta gesture so a splitter
// never moves. Mirrors key_frame_paced.
var private mouse_frame_paced : bool = false
var private mouse_frame_no : int = 0

def private reset_mouse_timeline() {
mouse_timeline |> clear()
mouse_play_idx = 0
mouse_playing = false
mouse_frame_paced = false
mouse_frame_no = 0
let cur = get_synth_cursor()
last_move_x = cur._1
last_move_y = cur._2
Expand Down Expand Up @@ -111,6 +120,11 @@ struct MouseEventWire {

struct MousePlayArgs {
events : array<MouseEventWire>
// When true, t_ms is read as a FRAME index (one event-group per frame) instead of
// wall-clock ms — mirrors imgui_key_play's frame_paced. Keeps a drag robust on slow or
// stalled frames: each frame advances the gesture one step, so press and the following
// moves never collapse into one frame (which lands a zero-delta drag on a splitter).
@optional frame_paced : bool = false
}

def public sort_mouse_play_events(var events : array<MouseEventWire>) {
Expand All @@ -126,6 +140,7 @@ def imgui_mouse_play(input : JsonValue?) : JsonValue? {
return JV((error = "no events")) if (empty(args.events))
sort_mouse_play_events(args.events)
reset_mouse_timeline()
mouse_frame_paced = args.frame_paced // after reset (which clears it); frame-index pacing for drags
mouse_timeline |> reserve(length(args.events))
for (w in args.events) {
var ev = MouseEvent(t_ms = w.t_ms)
Expand Down Expand Up @@ -158,7 +173,7 @@ def imgui_mouse_play(input : JsonValue?) : JsonValue? {
}

[live_command(description="Stop synthetic mouse playback (ImGui bypass) and release any held buttons.")]
def imgui_mouse_stop(input : JsonValue?) : JsonValue? {
def imgui_mouse_stop(_input : JsonValue?) : JsonValue? {
let was_playing = mouse_playing
var released = 0
for (b in range(0, 5)) {
Expand All @@ -183,7 +198,7 @@ struct MouseStatusResult {
}

[live_command(description="ImGui-bypass mouse playback status.")]
def imgui_mouse_status(input : JsonValue?) : JsonValue? {
def imgui_mouse_status(_input : JsonValue?) : JsonValue? {
let elapsed = mouse_playing ? int((get_uptime() - mouse_play_start) * 1000.0) : 0
let cur = get_synth_cursor()
var held = 0
Expand All @@ -206,7 +221,16 @@ def imgui_mouse_status(input : JsonValue?) : JsonValue? {

def private advance_mouse_timeline() {
return if (!mouse_playing)
let now_ms = int((get_uptime() - mouse_play_start) * 1000.0)
// Frame-paced gestures step one t_ms-group per frame; others use wall-clock. The lerp
// below interpolates by (now_ms - last_move_t)/span, so a frame-index `now_ms` makes it
// advance the cursor exactly one delta per frame — robust to any frame duration.
var now_ms = 0
if (mouse_frame_paced) {
now_ms = mouse_frame_no
mouse_frame_no++
} else {
now_ms = int((get_uptime() - mouse_play_start) * 1000.0)
}
while (mouse_play_idx < length(mouse_timeline)) {
let ev = mouse_timeline[mouse_play_idx]
break if (ev.t_ms > now_ms)
Expand Down Expand Up @@ -506,7 +530,7 @@ def imgui_key_play(input : JsonValue?) : JsonValue? {
}

[live_command(description="Stop synthetic key playback (ImGui bypass) and release any held keys.")]
def imgui_key_stop(input : JsonValue?) : JsonValue? {
def imgui_key_stop(_input : JsonValue?) : JsonValue? {
let was_playing = key_playing
let released = length(synth_held_keys)
release_held_keys()
Expand All @@ -525,7 +549,7 @@ struct KeyStatusResult {
}

[live_command(description="ImGui-bypass keyboard playback status.")]
def imgui_key_status(input : JsonValue?) : JsonValue? {
def imgui_key_status(_input : JsonValue?) : JsonValue? {
let elapsed = key_playing ? int((get_uptime() - key_play_start) * 1000.0) : 0
return JV(KeyStatusResult(
playing = key_playing,
Expand Down
28 changes: 14 additions & 14 deletions widgets/imgui_playwright.das
Original file line number Diff line number Diff line change
Expand Up @@ -501,22 +501,22 @@ def public drag(app : ImguiApp; target : string;
let cy = ((bb?["y"] ?? 0.0f) + (bb?["w"] ?? 0.0f)) * 0.5f
let ex = cx + dx
let ey = cy + dy
let drag_ms = (steps > 0 ? steps : 1) * 32
// Dwell at center BEFORE pressing: a move waypoint pins (cx,cy) at t=80, then
// the press fires 1ms later. Timestamps are STRICTLY increasing (the host sorts
// the timeline by t_ms with no tie-break, so a press sharing the dwell's t_ms
// could sort first and defeat it). The 1ms-into-a-drag_ms-long segment leaves the
// press essentially dead-center (sub-pixel drift), so it still lands on an 8px
// handle — without the dwell the press fires mid-travel and misses entirely.
// Frame-paced drag: t_ms is a FRAME index, so the host advances one cursor delta per
// frame and the press never collapses into the same frame as the sweep — a wall-clock
// drag does exactly that on a slow/stalled host (a coarse dt fires press + the full move
// together), landing a zero-delta gesture that never moves a splitter. ``steps`` is the
// number of sweep frames. A 2-frame dwell pins (cx,cy) on the handle before the press so
// it lands dead-center on a thin (8px) handle; timestamps stay strictly increasing.
let dframes = steps > 0 ? steps : 8
var events <- [
JV((t_ms = 0, kind = "move", x = cx, y = cy)),
JV((t_ms = 80, kind = "move", x = cx, y = cy)),
JV((t_ms = 81, kind = "button", button = button, action = "press")),
JV((t_ms = 81 + drag_ms, kind = "move", x = ex, y = ey)),
JV((t_ms = 181 + drag_ms, kind = "button", button = button, action = "release")),
JV((t_ms = 281 + drag_ms, kind = "move", x = ex, y = ey))
JV((t_ms = 0, kind = "move", x = cx, y = cy)),
JV((t_ms = 1, kind = "move", x = cx, y = cy)),
JV((t_ms = 2, kind = "button", button = button, action = "press")),
JV((t_ms = 2 + dframes, kind = "move", x = ex, y = ey)),
JV((t_ms = 3 + dframes, kind = "button", button = button, action = "release")),
JV((t_ms = 4 + dframes, kind = "move", x = ex, y = ey))
]
let resp = post_command(app, "imgui_mouse_play", JV((events = events)))
let resp = post_command(app, "imgui_mouse_play", JV((events = events, frame_paced = true)))
unsafe { delete events; }
if (resp == null) return JV((ok = false, error = "drag post failed: {target}"))
return JV((ok = true, value = target))
Expand Down
Loading