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, FrameInput, 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    /// Build this frame's viewport snapshot from sim and UI state.
37    fn frame_input(&mut self, sim: &Sim) -> FrameInput;
38    /// Variable-rate hook (tweens, audio, autosave timers).
39    fn on_frame(&mut self, _sim: &mut Sim, _dt: Duration) {}
40}
41
42pub struct RetainedRoot {
43    key: String,
44    generation: u64,
45}
46
47impl RetainedRoot {
48    pub fn new() -> Self {
49        Self {
50            key: format!(
51                "repame.retained_root.{}",
52                repose_core::unique_component_id()
53            ),
54            generation: 0,
55        }
56    }
57
58    pub fn call<F>(&mut self, sched: &mut repose_core::runtime::Scheduler, build: F) -> View
59    where
60        F: FnOnce(&mut repose_core::runtime::Scheduler) -> View,
61    {
62        let key = &self.key;
63        let epoch = repose_core::timer::frame_count();
64        let generation = self.generation;
65        repose_core::scope!(key, sched, [epoch, generation], { build(sched) })
66    }
67
68    pub fn clear(&mut self) {
69        self.generation = self.generation.wrapping_add(1);
70    }
71}
72
73impl Default for RetainedRoot {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79/// Desktop entry point. Mounts `root` with title and size.
80/// The root closure owns stepping and requests frames for continuity.
81/// The closure receives the live [`Scheduler`] each frame, whose
82/// `held_keys` / `window_focused` / `mouse_*` fields are the single
83/// polled hardware snapshot (GML `keyboard_check` /
84/// `mouse_check_button` parity).
85#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
86pub fn run_desktop(
87    title: &str,
88    size: (u32, u32),
89    mut root: impl FnMut(
90        &mut repose_core::runtime::Scheduler,
91        &repose_core::RenderContext,
92    ) -> repose_core::View
93    + 'static,
94) -> Result<()> {
95    let config = repose_app::AppConfig {
96        window_title: title.to_string(),
97        window_size: size,
98        ..Default::default()
99    };
100    let mut retained = RetainedRoot::new();
101    repose_platform::run_desktop_app_with_config(
102        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
103        config,
104    )?;
105    Ok(())
106}
107
108/// Web entry point. Mounts `root` with default options.
109#[cfg(target_arch = "wasm32")]
110pub fn run_web(
111    mut root: impl FnMut(
112        &mut repose_core::runtime::Scheduler,
113        &repose_core::RenderContext,
114    ) -> repose_core::View
115    + 'static,
116) -> Result<(), wasm_bindgen::JsValue> {
117    let mut options = repose_platform::web::WebOptions::new(None);
118    options.set_prevent_default(true);
119    let mut retained = RetainedRoot::new();
120    repose_platform::web::run_web_app(
121        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
122        options,
123    )
124}
125
126/// Android entry point. Mounts `root` with default options.
127#[cfg(target_os = "android")]
128pub fn run_android(
129    app: winit::platform::android::activity::AndroidApp,
130    mut root: impl FnMut(
131        &mut repose_core::runtime::Scheduler,
132        &repose_core::RenderContext,
133    ) -> repose_core::View
134    + 'static,
135) -> Result<()> {
136    let mut retained = RetainedRoot::new();
137    repose_platform::android::run_android_app_with_options(
138        app,
139        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
140        repose_app::AndroidOptions::default(),
141    )
142}
143
144/// Gamepad polling on the shared platform backend. Events feed
145/// `ReposeRuntime::handle_gamepad` (UI nav) and `rt.gamepads` (gameplay).
146pub struct GamepadPoller {
147    backend: Option<Box<dyn GamepadBackend>>,
148    connected: HashSet<u32>,
149}
150
151impl GamepadPoller {
152    pub fn new() -> Self {
153        Self {
154            backend: create_backend().map(|b| Box::new(b) as Box<dyn GamepadBackend>),
155            // Boot-plugged pads report on first poll; no press needed.
156            connected: HashSet::new(),
157        }
158    }
159
160    /// Drain hardware events since the last call. Tracks connection state
161    /// so `connected_ids` stays live.
162    pub fn poll(&mut self) -> Vec<GamepadEvent> {
163        let Some(backend) = &mut self.backend else {
164            return Vec::new();
165        };
166        let events = backend.poll();
167        for ev in &events {
168            match ev {
169                GamepadEvent::Connected { id, .. } => {
170                    self.connected.insert(id.0);
171                }
172                GamepadEvent::Disconnected { id } => {
173                    self.connected.remove(&id.0);
174                }
175                GamepadEvent::Button { id, .. } | GamepadEvent::Axis { id, .. } => {
176                    self.connected.insert(id.0);
177                }
178            }
179        }
180        events
181    }
182
183    pub fn connected_ids(&self) -> Vec<GamepadId> {
184        let mut ids: Vec<GamepadId> = self.connected.iter().copied().map(GamepadId).collect();
185        ids.sort_by_key(|id| id.0);
186        ids
187    }
188
189    pub fn connected_count(&self) -> usize {
190        self.connected.len()
191    }
192}
193
194impl Default for GamepadPoller {
195    fn default() -> Self {
196        Self::new()
197    }
198}