Skip to main content

repame_shell/
lib.rs

1//! App wiring: Repose platform runners plus sim and gamepad helpers.
2/// Games provide the root view; `run_*` runners mount only, stepping
3/// stays game-side via [`Sim::step`].
4use std::collections::HashSet;
5use web_time::Duration;
6
7use anyhow::Result;
8pub use repame_sim::{Sim, SimTime};
9pub use repame_sprite::{Camera2d, PickEvent, SpriteInstance};
10use repose_core::View;
11pub use repose_core::input::{GamepadEvent, GamepadId};
12use repose_platform::gamepad::{GamepadBackend, create_backend};
13
14mod gesture;
15mod haptics;
16mod levels;
17mod motion;
18mod pads;
19mod quit;
20mod shortcuts;
21mod staging;
22pub use gesture::{on_first_user_gesture, user_gesture_seen};
23pub use haptics::{RUMBLE_REFRESH, RUMBLE_UPLOAD_MS, RumbleBridge};
24pub use levels::apply_scheduler_levels;
25pub use motion::{MotionPoller, PadMotion};
26pub use pads::{PadBank, PadBridge, TRIGGER_HELD};
27pub use quit::Quit;
28pub use shortcuts::{
29    SharedEdges, ShortcutEdges, game_shortcut_map, install_game_shortcuts, install_handler_into,
30    install_map, shared_edges, shortcut_handler, take as take_shortcut_edges,
31};
32pub use staging::Staging;
33
34/// Per-frame hooks the game implements.
35pub trait ShellHooks {
36    /// Variable-rate hook (tweens, audio, autosave timers).
37    fn on_frame(&mut self, _sim: &mut Sim, _dt: Duration) {}
38}
39
40pub struct RetainedRoot {
41    key: String,
42    generation: u64,
43}
44
45impl RetainedRoot {
46    pub fn new() -> Self {
47        Self {
48            key: format!(
49                "repame.retained_root.{}",
50                repose_core::unique_component_id()
51            ),
52            generation: 0,
53        }
54    }
55
56    pub fn call<F>(&mut self, sched: &mut repose_core::runtime::Scheduler, build: F) -> View
57    where
58        F: FnOnce(&mut repose_core::runtime::Scheduler) -> View,
59    {
60        let key = &self.key;
61        let epoch = repose_core::timer::frame_count();
62        let generation = self.generation;
63        repose_core::scope!(key, sched, [epoch, generation], { build(sched) })
64    }
65
66    pub fn clear(&mut self) {
67        self.generation = self.generation.wrapping_add(1);
68    }
69}
70
71impl Default for RetainedRoot {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77/// Desktop entry point. Mounts `root` with title and size.
78/// The root closure owns stepping and requests frames for continuity.
79/// The closure receives the live [`Scheduler`] each frame, whose
80/// `held_keys` / `window_focused` / `mouse_*` fields are the single
81/// polled hardware snapshot (GML `keyboard_check` /
82/// `mouse_check_button` parity).
83#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
84pub fn run_desktop(
85    title: &str,
86    size: (u32, u32),
87    mut root: impl FnMut(
88        &mut repose_core::runtime::Scheduler,
89        &repose_core::RenderContext,
90    ) -> repose_core::View
91    + 'static,
92) -> Result<()> {
93    let config = repose_app::AppConfig {
94        window_title: title.to_string(),
95        window_size: size,
96        ..Default::default()
97    };
98    let mut retained = RetainedRoot::new();
99    repose_platform::run_desktop_app_with_config(
100        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
101        config,
102    )?;
103    Ok(())
104}
105
106/// Web entry point. Mounts `root` with default options.
107#[cfg(target_arch = "wasm32")]
108pub fn run_web(
109    mut root: impl FnMut(
110        &mut repose_core::runtime::Scheduler,
111        &repose_core::RenderContext,
112    ) -> repose_core::View
113    + 'static,
114) -> Result<(), wasm_bindgen::JsValue> {
115    let mut options = repose_platform::web::WebOptions::new(None);
116    options.set_prevent_default(true);
117    let mut retained = RetainedRoot::new();
118    repose_platform::web::run_web_app(
119        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
120        options,
121    )
122}
123
124/// Android entry point. Mounts `root` with default options.
125#[cfg(target_os = "android")]
126pub fn run_android(
127    app: winit::platform::android::activity::AndroidApp,
128    mut root: impl FnMut(
129        &mut repose_core::runtime::Scheduler,
130        &repose_core::RenderContext,
131    ) -> repose_core::View
132    + 'static,
133) -> Result<()> {
134    let mut retained = RetainedRoot::new();
135    repose_platform::android::run_android_app_with_options(
136        app,
137        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
138        repose_app::AndroidOptions::default(),
139    )
140}
141
142/// Gamepad polling on the shared platform backend. Events feed
143/// `ReposeRuntime::handle_gamepad` (UI nav) and `rt.gamepads` (gameplay).
144pub struct GamepadPoller {
145    backend: Option<Box<dyn GamepadBackend>>,
146    connected: HashSet<u32>,
147}
148
149impl GamepadPoller {
150    pub fn new() -> Self {
151        Self {
152            backend: create_backend().map(|b| Box::new(b) as Box<dyn GamepadBackend>),
153            // Boot-plugged pads report on first poll; no press needed.
154            connected: HashSet::new(),
155        }
156    }
157
158    /// Drain hardware events since the last call. Tracks connection state
159    /// so `connected_ids` stays live.
160    pub fn poll(&mut self) -> Vec<GamepadEvent> {
161        let Some(backend) = &mut self.backend else {
162            return Vec::new();
163        };
164        let events = backend.poll();
165        for ev in &events {
166            match ev {
167                GamepadEvent::Connected { id, .. } => {
168                    self.connected.insert(id.0);
169                }
170                GamepadEvent::Disconnected { id } => {
171                    self.connected.remove(&id.0);
172                }
173                GamepadEvent::Button { id, .. } | GamepadEvent::Axis { id, .. } => {
174                    self.connected.insert(id.0);
175                }
176            }
177        }
178        events
179    }
180
181    pub fn connected_ids(&self) -> Vec<GamepadId> {
182        let mut ids: Vec<GamepadId> = self.connected.iter().copied().map(GamepadId).collect();
183        ids.sort_by_key(|id| id.0);
184        ids
185    }
186
187    pub fn connected_count(&self) -> usize {
188        self.connected.len()
189    }
190}
191
192impl Default for GamepadPoller {
193    fn default() -> Self {
194        Self::new()
195    }
196}