Skip to main content

repame_shell/
lib.rs

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