Skip to main content

repame_shell/
lib.rs

1//! App wiring: Repose platform runners + sim/gamepad helpers.
2//!
3//! A game provides a root view; [`ShellHooks`] is the game-side per-frame
4//! contract (snapshot production + variable-rate hook) that the game's own
5//! root closure drives. The `run_*` runners below are thin mounts only:
6//! they do not call `ShellHooks` or step the sim — stepping stays game-side
7//! via [`Sim::step`] (see the rozvp pilot runner for the reference wiring).
8//! Per-game UI stays in the game crate as Repose views.
9
10use std::collections::HashSet;
11use web_time::Duration;
12
13use anyhow::Result;
14pub use repame_sim::{Sim, SimTime};
15pub use repame_sprite::{Camera2d, FrameInput, PickEvent, SpriteInstance};
16pub use repose_core::input::{GamepadEvent, GamepadId};
17use repose_platform::gamepad::{GamepadBackend, create_backend};
18
19/// Per-frame hooks the game implements. Snapshot production (`frame_input`)
20/// is the only required render coupling: plain data out, no renderer types.
21pub trait ShellHooks {
22    /// Build this frame's viewport snapshot from sim + UI state.
23    fn frame_input(&mut self, sim: &Sim) -> FrameInput;
24    /// Fixed-step systems run inside [`Sim`]; this is the per-frame
25    /// variable-rate hook (tweens, audio triggers, autosave timers).
26    fn on_frame(&mut self, _sim: &mut Sim, _dt: Duration) {}
27}
28
29/// Desktop entry point. Mounts `root` on the repose-platform runner with
30/// the given window title/size. The root closure owns stepping (sim,
31/// rigs) and must call `request_frame()` for continuous frames; see
32/// the rozvp pilot runner for the reference wiring.
33#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
34pub fn run_desktop(
35    title: &str,
36    size: (u32, u32),
37    root: impl FnMut(
38        &mut repose_core::runtime::Scheduler,
39        &repose_core::RenderContext,
40    ) -> repose_core::View
41    + 'static,
42) -> Result<()> {
43    let config = repose_app::AppConfig {
44        window_title: title.to_string(),
45        window_size: size,
46        ..Default::default()
47    };
48    repose_platform::run_desktop_app_with_config(root, config)?;
49    Ok(())
50}
51
52/// Web entry point. Mounts `root` on the repose-platform web runner with
53/// default options (prevent-default on, default canvas).
54#[cfg(target_arch = "wasm32")]
55pub fn run_web(
56    root: impl FnMut(
57        &mut repose_core::runtime::Scheduler,
58        &repose_core::RenderContext,
59    ) -> repose_core::View
60    + 'static,
61) -> Result<(), wasm_bindgen::JsValue> {
62    let mut options = repose_platform::web::WebOptions::new(None);
63    options.set_prevent_default(true);
64    repose_platform::web::run_web_app(root, options)
65}
66
67/// Android entry point. Mounts `root` on the repose-platform android
68/// runner with default options. Logging + window insets stay app-side.
69#[cfg(target_os = "android")]
70pub fn run_android(
71    app: winit::platform::android::activity::AndroidApp,
72    root: impl FnMut(
73        &mut repose_core::runtime::Scheduler,
74        &repose_core::RenderContext,
75    ) -> repose_core::View
76    + 'static,
77) -> Result<()> {
78    repose_platform::android::run_android_app_with_options(
79        app,
80        root,
81        repose_app::AndroidOptions::default(),
82    )
83}
84
85/// Gamepad polling unified on the shared `repose-platform` backend: one
86/// button layout, one deadzone, one mapping policy for repose apps and
87/// repame games alike. Events feed straight into
88/// `ReposeRuntime::handle_gamepad` (UI nav) and `rt.gamepads` (gameplay).
89pub struct GamepadPoller {
90    backend: Option<Box<dyn GamepadBackend>>,
91    connected: HashSet<u32>,
92}
93
94impl GamepadPoller {
95    pub fn new() -> Self {
96        Self {
97            backend: create_backend().map(|b| Box::new(b) as Box<dyn GamepadBackend>),
98            connected: HashSet::new(),
99        }
100    }
101
102    /// Drain hardware events since the last call. Tracks connection state
103    /// so [`GamepadPoller::connected_ids`] works even for pads connected
104    /// before startup: gilrs only emits `Connected` on its own schedule,
105    /// so any button/axis event from an unknown id implicitly marks that
106    /// pad connected (first input at the latest).
107    pub fn poll(&mut self) -> Vec<GamepadEvent> {
108        let Some(backend) = &mut self.backend else {
109            return Vec::new();
110        };
111        let events = backend.poll();
112        for ev in &events {
113            match ev {
114                GamepadEvent::Connected { id, .. } => {
115                    self.connected.insert(id.0);
116                }
117                GamepadEvent::Disconnected { id } => {
118                    self.connected.remove(&id.0);
119                }
120                GamepadEvent::Button { id, .. } | GamepadEvent::Axis { id, .. } => {
121                    self.connected.insert(id.0);
122                }
123            }
124        }
125        events
126    }
127
128    pub fn connected_ids(&self) -> Vec<GamepadId> {
129        self.connected.iter().copied().map(GamepadId).collect()
130    }
131
132    pub fn connected_count(&self) -> usize {
133        self.connected.len()
134    }
135}
136
137impl Default for GamepadPoller {
138    fn default() -> Self {
139        Self::new()
140    }
141}