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};
10pub use repose_core::input::{GamepadEvent, GamepadId};
11use repose_platform::gamepad::{GamepadBackend, create_backend};
12
13mod audio;
14mod levels;
15mod pads;
16mod shortcuts;
17mod staging;
18mod store;
19pub use audio::drain_cues;
20pub use levels::apply_scheduler_levels;
21pub use pads::{PadBank, PadBridge, TRIGGER_HELD};
22pub use shortcuts::{SharedEdges, ShortcutEdges, game_shortcut_map, install_handler_into, install_into, install_map, shared_edges, take as take_shortcut_edges};
23pub use staging::Staging;
24pub use store::{load_json, save_file_path, store_json};
25
26/// Per-frame hooks the game implements.
27pub trait ShellHooks {
28    /// Build this frame's viewport snapshot from sim and UI state.
29    fn frame_input(&mut self, sim: &Sim) -> FrameInput;
30    /// Variable-rate hook (tweens, audio, autosave timers).
31    fn on_frame(&mut self, _sim: &mut Sim, _dt: Duration) {}
32}
33
34/// Desktop entry point. Mounts `root` with title and size.
35/// The root closure owns stepping and requests frames for continuity.
36/// The closure receives the live [`Scheduler`] each frame, whose
37/// `held_keys` / `window_focused` / `mouse_*` fields are the single
38/// polled hardware snapshot (GML `keyboard_check` /
39/// `mouse_check_button` parity).
40#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
41pub fn run_desktop(
42    title: &str,
43    size: (u32, u32),
44    root: impl FnMut(
45        &mut repose_core::runtime::Scheduler,
46        &repose_core::RenderContext,
47    ) -> repose_core::View
48    + 'static,
49) -> Result<()> {
50    let config = repose_app::AppConfig {
51        window_title: title.to_string(),
52        window_size: size,
53        ..Default::default()
54    };
55    repose_platform::run_desktop_app_with_config(root, config)?;
56    Ok(())
57}
58
59/// Web entry point. Mounts `root` with default options.
60#[cfg(target_arch = "wasm32")]
61pub fn run_web(
62    root: impl FnMut(
63        &mut repose_core::runtime::Scheduler,
64        &repose_core::RenderContext,
65    ) -> repose_core::View
66    + 'static,
67) -> Result<(), wasm_bindgen::JsValue> {
68    let mut options = repose_platform::web::WebOptions::new(None);
69    options.set_prevent_default(true);
70    repose_platform::web::run_web_app(root, options)
71}
72
73/// Android entry point. Mounts `root` with default options.
74#[cfg(target_os = "android")]
75pub fn run_android(
76    app: winit::platform::android::activity::AndroidApp,
77    root: impl FnMut(
78        &mut repose_core::runtime::Scheduler,
79        &repose_core::RenderContext,
80    ) -> repose_core::View
81    + 'static,
82) -> Result<()> {
83    repose_platform::android::run_android_app_with_options(
84        app,
85        root,
86        repose_app::AndroidOptions::default(),
87    )
88}
89
90/// Gamepad polling on the shared platform backend. Events feed
91/// `ReposeRuntime::handle_gamepad` (UI nav) and `rt.gamepads` (gameplay).
92pub struct GamepadPoller {
93    backend: Option<Box<dyn GamepadBackend>>,
94    connected: HashSet<u32>,
95}
96
97impl GamepadPoller {
98    pub fn new() -> Self {
99        Self {
100            backend: create_backend().map(|b| Box::new(b) as Box<dyn GamepadBackend>),
101            // Boot-plugged pads report on first poll; no press needed.
102            connected: HashSet::new(),
103        }
104    }
105
106    /// Drain hardware events since the last call. Tracks connection state
107    /// so `connected_ids` stays live.
108    pub fn poll(&mut self) -> Vec<GamepadEvent> {
109        let Some(backend) = &mut self.backend else {
110            return Vec::new();
111        };
112        let events = backend.poll();
113        for ev in &events {
114            match ev {
115                GamepadEvent::Connected { id, .. } => {
116                    self.connected.insert(id.0);
117                }
118                GamepadEvent::Disconnected { id } => {
119                    self.connected.remove(&id.0);
120                }
121                GamepadEvent::Button { id, .. } | GamepadEvent::Axis { id, .. } => {
122                    self.connected.insert(id.0);
123                }
124            }
125        }
126        events
127    }
128
129    pub fn connected_ids(&self) -> Vec<GamepadId> {
130        let mut ids: Vec<GamepadId> = self.connected.iter().copied().map(GamepadId).collect();
131        ids.sort_by_key(|id| id.0);
132        ids
133    }
134
135    pub fn connected_count(&self) -> usize {
136        self.connected.len()
137    }
138}
139
140impl Default for GamepadPoller {
141    fn default() -> Self {
142        Self::new()
143    }
144}