From 395a322adc73402eb73f21f98d83c708a7badb4a Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 8 May 2026 19:37:22 -0700 Subject: [PATCH] feat(android): add double-tap auto-move for touch input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors handle_double_click for the touch pipeline. A double-tap on a face-up card fires MoveRequestEvent to the best legal destination using the same priority order (foundation first, tableau second; stack move as priority 2 when the tapped card is a stack base). Implementation: - handle_double_tap reads TouchPhase::Ended events. When drag.active_touch_id is set and drag.committed is false, the touch ended without crossing the drag threshold = pure tap. The top card ID from drag.cards is used as the tracking key. - DOUBLE_TAP_WINDOW = 0.5s (wider than DOUBLE_CLICK_WINDOW = 0.35s; touch screens have higher input latency; pinned by a const-assert test). - System is inserted between touch_follow_drag and touch_end_drag in the .chain() so drag state is readable before touch_end_drag clears it. - touch_end_drag's uncommitted-tap cleanup path still fires after handle_double_tap — the drag.clear() + StateChangedEvent are harmless in sequence with a MoveRequestEvent already queued. 1 new test (1283 total): double_tap_window_is_wider_than_double_click_window (compile-time const assert). Co-Authored-By: Claude Sonnet 4.6 --- solitaire_engine/src/input_plugin.rs | 134 ++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index a02a1bd..10245d3 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -105,6 +105,7 @@ impl Plugin for InputPlugin { // Touch drag pipeline (parallel path through DragState). touch_start_drag, touch_follow_drag, + handle_double_tap, // before touch_end_drag: reads drag state pre-clear touch_end_drag.before(GameMutation), ) .chain(), @@ -1204,12 +1205,16 @@ fn pile_drop_rect(pile: &PileType, layout: &Layout, game: &GameState) -> (Vec2, } // --------------------------------------------------------------------------- -// Task #27 — Double-click to auto-move +// Task #27 — Double-click / double-tap to auto-move // --------------------------------------------------------------------------- /// Maximum seconds between two clicks to count as a double-click. const DOUBLE_CLICK_WINDOW: f32 = 0.35; +/// Maximum seconds between two taps to count as a double-tap. +/// Slightly wider than the mouse window — touch screens have higher latency. +const DOUBLE_TAP_WINDOW: f32 = 0.5; + /// Find the best legal destination for `card` — Foundation first, then Tableau. /// /// Returns `None` if no legal move exists from the card's current location. @@ -1363,6 +1368,124 @@ fn handle_double_click( } } +// --------------------------------------------------------------------------- +// Task #27b — Double-tap to auto-move (touch equivalent of double-click) +// --------------------------------------------------------------------------- + +/// System that detects double-taps on face-up cards and fires `MoveRequestEvent` +/// to the best legal destination — the touch equivalent of [`handle_double_click`]. +/// +/// Must run **before** `touch_end_drag` in the system chain. At +/// `TouchPhase::Ended` the drag state still holds `active_touch_id`, +/// `cards`, and `origin_pile`; once `touch_end_drag` fires those fields +/// are cleared and the tap/drag distinction is permanently lost. +/// +/// A pure tap is identified by `drag.active_touch_id.is_some() && +/// !drag.committed`: the touch began (so `touch_start_drag` populated +/// `drag`) but the drag threshold was never crossed. +/// +/// Move priority matches [`handle_double_click`]: +/// 1. Move the single top card to its best foundation (or tableau). +/// 2. If no single-card move exists and the selection spans multiple +/// face-up cards, move the whole stack to the best tableau column. +/// 3. If both priorities fail, fire `MoveRejectedEvent` for audio + shake +/// feedback. +#[allow(clippy::too_many_arguments)] +fn handle_double_tap( + mut touch_events: MessageReader, + paused: Option>, + time: Res