P6.1: lock input for the full in-flight animation window
A swipe that began while a move animation was playing could still commit: mouse_down latched the drag unconditionally and the animation-active check sat at mouse_up, so a press made mid-animation committed once the timeline finished before release — against a board mid-transition. Gate input at gesture START instead. Add a pure `accepts_input(anim)` predicate (false while a timeline is active) and check it at mouse_down: a press begun mid-animation is dropped and never latches a drag, so it cannot commit when the animation later settles. The now-dead mouse_up gate is removed. Animation visuals and the logical model are unchanged. Extend tests/anim_plan.sx to assert accepts_input rejects for the whole window (idle accept / busy reject / settled accept) and that press-gating drops the exact failure gesture a release-gate would let through.
This commit is contained in:
@@ -184,3 +184,13 @@ BoardAnim :: struct {
|
||||
AnimPhase.{ kind = .done, round = 0, t = 1.0 }
|
||||
}
|
||||
}
|
||||
|
||||
// Input gate: the board accepts a new swipe/tap gesture only when no move
|
||||
// animation is in flight. The view checks this at gesture START (mouse_down),
|
||||
// not at commit (mouse_up), so a gesture begun while a timeline is playing never
|
||||
// latches a drag and so cannot commit when the animation later settles. Input
|
||||
// resumes once `tick` clears `active` at the end of the timeline. A null anim
|
||||
// (no animation layer wired) always accepts.
|
||||
accepts_input :: (anim: *BoardAnim) -> bool {
|
||||
anim == null or !anim.active
|
||||
}
|
||||
|
||||
@@ -400,6 +400,12 @@ impl View for BoardView {
|
||||
self.compute_layout(frame);
|
||||
if event.* == {
|
||||
case .mouse_down: (d) {
|
||||
// Gate input at gesture START: while a move animation is in
|
||||
// flight the board ignores new gestures for the WHOLE in-flight
|
||||
// window, so a press begun mid-animation never latches a drag and
|
||||
// so can't commit when the animation later ends. The press is
|
||||
// still consumed; input resumes once the timeline settles.
|
||||
if !accepts_input(self.anim) { return true; }
|
||||
self.drag.begin(d.position);
|
||||
return true;
|
||||
}
|
||||
@@ -407,9 +413,6 @@ impl View for BoardView {
|
||||
if !self.drag.active { return false; }
|
||||
start := self.drag.start;
|
||||
self.drag.clear();
|
||||
// Ignore swipes while a move is still animating so two timelines
|
||||
// never overlap; the model is already settled by then either way.
|
||||
if self.anim != null and self.anim.active { return true; }
|
||||
if intent := swipe_intent(@self.layout, start, d.position) {
|
||||
mv := plan_and_commit(self.board, intent.a, intent.b);
|
||||
if self.anim != null { self.anim.begin(mv); }
|
||||
|
||||
BIN
goldens/p6_inputlock_board.png
Normal file
BIN
goldens/p6_inputlock_board.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 MiB |
@@ -10,6 +10,10 @@
|
||||
// each later round starts on the prior round's settled board), and the last
|
||||
// round's `after` equals `final`;
|
||||
// - an illegal swap records no rounds and leaves the board untouched.
|
||||
// It also guards the P6.1 input-lock fix: `accepts_input` rejects a gesture for
|
||||
// the FULL in-flight animation window (mouse_down → settle), so a swipe begun
|
||||
// while a move animates never latches a drag and never commits — even if it is
|
||||
// released after the timeline ends.
|
||||
// No rendering — it calls exactly what BoardView.handle_event calls. Links headless
|
||||
// like tests/swipe_commit.sx; avoids tests/test.sx (its trace.sx pulls in a second
|
||||
// `Frame` that collides with the UI one). Failure is a non-zero exit code.
|
||||
@@ -108,6 +112,42 @@ main :: () -> s32 {
|
||||
if bi2.score != 0 { fails += 1; }
|
||||
if bi2.moves_made != 0 { fails += 1; }
|
||||
|
||||
// ── Input gate: locked for the FULL in-flight animation ─────────────────
|
||||
// The view begins a drag on mouse_down only when `accepts_input` is true,
|
||||
// and commits on mouse_up only if a drag latched. So a gesture that BEGINS
|
||||
// while a move animates must NEVER commit — even if it is released after the
|
||||
// animation has fully settled. This guards the P6.1 input-lock fix.
|
||||
print("== input gate: locked while animating ==\n");
|
||||
gate : BoardAnim = ---;
|
||||
gate.init();
|
||||
idle_ok := accepts_input(@gate); // no move in flight → accept
|
||||
gate.begin(move); // a legal move's timeline starts
|
||||
busy_ok := accepts_input(@gate); // mouse_down DURING animation → reject
|
||||
while gate.active { gate.tick(0.05); } // player holds; timeline plays out
|
||||
settled_ok := accepts_input(@gate); // animation fully settled → accept
|
||||
print("accepts idle {} busy {} settled {}\n", idle_ok, busy_ok, settled_ok);
|
||||
if !idle_ok { fails += 1; }
|
||||
if busy_ok { fails += 1; } // MUST be locked while animating
|
||||
if !settled_ok { fails += 1; }
|
||||
|
||||
// The board MUST decide a gesture at PRESS, not at RELEASE. Over the exact
|
||||
// failure scenario — a gesture PRESSED while animating and RELEASED after the
|
||||
// timeline has settled — the two policies diverge:
|
||||
// release-gate: commit unless animating AT RELEASE → COMMITS (the timeline
|
||||
// finished first), letting input slip through mid-transition.
|
||||
// press-gate: latch only if input accepted AT PRESS → DROPS, because
|
||||
// input was locked for the whole window the timeline ran.
|
||||
gate.init();
|
||||
gate.begin(move);
|
||||
accept_at_press := accepts_input(@gate); // mouse_down while animating
|
||||
while gate.active { gate.tick(0.05); }
|
||||
accept_at_release := accepts_input(@gate); // mouse_up after settle
|
||||
release_gate_commits := accept_at_release;
|
||||
press_gate_commits := accept_at_press;
|
||||
print("release_gate_commits {} press_gate_commits {}\n", release_gate_commits, press_gate_commits);
|
||||
if !release_gate_commits { fails += 1; } // the scenario release-gating lets through
|
||||
if press_gate_commits { fails += 1; } // the board press-gates: MUST NOT commit
|
||||
|
||||
if fails == 0 {
|
||||
print("ok: animation layer leaves the model result unchanged\n");
|
||||
return 0;
|
||||
|
||||
@@ -14,4 +14,7 @@ YOYYROBB
|
||||
OROBPPRB
|
||||
== illegal swap: untouched ==
|
||||
legal false rounds 0 score 0 moves 0
|
||||
== input gate: locked while animating ==
|
||||
accepts idle true busy false settled true
|
||||
release_gate_commits true press_gate_commits false
|
||||
ok: animation layer leaves the model result unchanged
|
||||
|
||||
Reference in New Issue
Block a user