From 7411468e1091c54e1e10dee9a7f6716f3f4b9452 Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 12 May 2026 16:03:20 -0700 Subject: [PATCH] fix(engine): extend touch scroll to achievements and stats panels via generic helper Extracts touch_scroll_panel into ui_modal.rs and wires it to SettingsPanelScrollable, AchievementsScrollable, and StatsScrollable so all three panels respond to finger swipe on Android. Co-Authored-By: Claude Sonnet 4.6 --- solitaire_engine/src/achievement_plugin.rs | 2 ++ solitaire_engine/src/settings_plugin.rs | 40 +-------------------- solitaire_engine/src/stats_plugin.rs | 4 ++- solitaire_engine/src/ui_modal.rs | 42 ++++++++++++++++++++++ 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/solitaire_engine/src/achievement_plugin.rs b/solitaire_engine/src/achievement_plugin.rs index 5a82b55..9419a6e 100644 --- a/solitaire_engine/src/achievement_plugin.rs +++ b/solitaire_engine/src/achievement_plugin.rs @@ -116,6 +116,7 @@ impl Plugin for AchievementPlugin { // achievements-scroll system also runs cleanly under // `MinimalPlugins` in tests. .add_message::() + .add_message::() // Run after GameMutation (so GameWonEvent is available), after // StatsUpdate (so stats reflect this win), and after ProgressUpdate // (so daily_challenge_streak is up to date for daily_devotee). @@ -139,6 +140,7 @@ impl Plugin for AchievementPlugin { .add_systems(Update, toggle_achievements_screen) .add_systems(Update, handle_achievements_close_button) .add_systems(Update, scroll_achievements_panel) + .add_systems(Update, crate::ui_modal::touch_scroll_panel::) // Event-driven unlock: observe `ReplayPlaybackState` and unlock // `cinephile` the first time playback runs to natural completion. // Reads the resource via `Option>` so headless tests that diff --git a/solitaire_engine/src/settings_plugin.rs b/solitaire_engine/src/settings_plugin.rs index c476b6a..1f4fe93 100644 --- a/solitaire_engine/src/settings_plugin.rs +++ b/solitaire_engine/src/settings_plugin.rs @@ -12,7 +12,6 @@ use std::path::PathBuf; use bevy::input::mouse::{MouseScrollUnit, MouseWheel}; -use bevy::input::touch::{TouchInput, TouchPhase}; use bevy::prelude::*; use bevy::ui::{ComputedNode, UiGlobalTransform}; use bevy::window::{WindowMoved, WindowResized}; @@ -371,7 +370,7 @@ impl Plugin for SettingsPlugin { handle_volume_keys, toggle_settings_screen, scroll_settings_panel, - touch_scroll_settings_panel, + crate::ui_modal::touch_scroll_panel::, record_window_geometry_changes, persist_window_geometry_after_debounce, ), @@ -1370,43 +1369,6 @@ fn scroll_settings_panel( } } -/// Scrolls the settings panel in response to touch pan gestures (Android). -/// Tracks the most recent touch Y so each `Moved` event's delta can be -/// applied to `ScrollPosition`. `MouseWheel` handles desktop; this system -/// fills the gap for single-finger swipe on touchscreen devices. -fn touch_scroll_settings_panel( - mut touch_evr: MessageReader, - screen: Res, - mut scrollables: Query<&mut ScrollPosition, With>, - mut last_y: Local>, -) { - if !screen.0 { - touch_evr.clear(); - *last_y = None; - return; - } - for event in touch_evr.read() { - match event.phase { - TouchPhase::Started => { - *last_y = Some(event.position.y); - } - TouchPhase::Moved => { - if let Some(prev) = *last_y { - let delta = event.position.y - prev; - for mut sp in scrollables.iter_mut() { - // Swiping up (delta < 0) scrolls content down (sp.y increases). - sp.0.y = (sp.0.y - delta).max(0.0); - } - } - *last_y = Some(event.position.y); - } - TouchPhase::Ended | TouchPhase::Canceled => { - *last_y = None; - } - } - } -} - // --------------------------------------------------------------------------- // Window geometry persistence // --------------------------------------------------------------------------- diff --git a/solitaire_engine/src/stats_plugin.rs b/solitaire_engine/src/stats_plugin.rs index 28aa63c..2fc524f 100644 --- a/solitaire_engine/src/stats_plugin.rs +++ b/solitaire_engine/src/stats_plugin.rs @@ -203,6 +203,7 @@ impl Plugin for StatsPlugin { // `DefaultPlugins`; register it explicitly so the stats-scroll // system also runs cleanly under `MinimalPlugins` in tests. .add_message::() + .add_message::() // record_abandoned must read `move_count` BEFORE handle_new_game // clobbers it with a fresh game. These are NOT in StatsUpdate because // StatsUpdate (as a set) is ordered after GameMutation by external @@ -238,7 +239,8 @@ impl Plugin for StatsPlugin { ) .chain(), ) - .add_systems(Update, scroll_stats_panel); + .add_systems(Update, scroll_stats_panel) + .add_systems(Update, crate::ui_modal::touch_scroll_panel::); } } diff --git a/solitaire_engine/src/ui_modal.rs b/solitaire_engine/src/ui_modal.rs index c86e310..5a3da0c 100644 --- a/solitaire_engine/src/ui_modal.rs +++ b/solitaire_engine/src/ui_modal.rs @@ -48,6 +48,7 @@ //! ); //! ``` +use bevy::input::touch::{TouchInput, TouchPhase}; use bevy::prelude::*; use bevy::ui::{ComputedNode, UiGlobalTransform}; use bevy::window::PrimaryWindow; @@ -390,6 +391,47 @@ pub fn spawn_modal_button( }); } +// --------------------------------------------------------------------------- +// Generic touch-scroll helper +// --------------------------------------------------------------------------- + +/// Scrolls any `Overflow::scroll_y()` panel marked with `M` via single-finger +/// touch pan. Add this as a system for each scrollable modal panel: +/// +/// ```ignore +/// app.add_message::() +/// .add_systems(Update, touch_scroll_panel::); +/// ``` +/// +/// On desktop `TouchInput` events never fire, so the system is a no-op. +pub fn touch_scroll_panel( + mut touch_evr: MessageReader, + mut scrollables: Query<&mut ScrollPosition, With>, + mut last_y: Local>, +) { + for event in touch_evr.read() { + match event.phase { + TouchPhase::Started => { + *last_y = Some(event.position.y); + } + TouchPhase::Moved => { + if let Some(prev) = *last_y { + let delta = event.position.y - prev; + for mut sp in scrollables.iter_mut() { + // Swiping up (delta < 0 in screen coords) scrolls + // content down, so sp.y increases. + sp.0.y = (sp.0.y - delta).max(0.0); + } + } + *last_y = Some(event.position.y); + } + TouchPhase::Ended | TouchPhase::Canceled => { + *last_y = None; + } + } + } +} + // --------------------------------------------------------------------------- // Helpers + paint system // ---------------------------------------------------------------------------