repame-shell 0.3.25

Repose platform runner wrappers, sim stepping, viewport mount, and gamepad polling
Documentation
//! App wiring: Repose platform runners plus sim, gamepad and portal helpers.
/// Games provide the root view; `run_*` runners mount only, stepping
/// stays game-side via [`Sim::step`].
use std::collections::HashSet;
use web_time::Duration;

use anyhow::Result;
pub use repame_sim::{Sim, SimTime};
pub use repame_sprite::{Camera2d, PickEvent, SpriteInstance};
use repose_core::View;
pub use repose_core::input::{GamepadEvent, GamepadId};
use repose_platform::gamepad::{GamepadBackend, create_backend};

mod gesture;
mod haptics;
mod levels;
mod motion;
mod pads;
pub mod portal;
mod quit;
mod shortcuts;
mod staging;
pub use gesture::{on_first_user_gesture, user_gesture_seen};
pub use haptics::{RUMBLE_REFRESH, RUMBLE_UPLOAD_MS, RumbleBridge};
pub use levels::apply_scheduler_levels;
pub use motion::{MotionPoller, PadMotion};
pub use pads::{PadBank, PadBridge, TRIGGER_HELD};
pub use portal::{AdKind, AdOutcome, GameState, PortalHost, PortalReport, Suspend};
pub use quit::Quit;
pub use shortcuts::{
    SharedEdges, ShortcutEdges, game_shortcut_map, install_game_shortcuts, install_handler_into,
    install_map, shared_edges, shortcut_handler, take as take_shortcut_edges,
};
pub use staging::Staging;

/// Per-frame hooks the game implements.
pub trait ShellHooks {
    /// Variable-rate hook (tweens, audio, autosave timers).
    fn on_frame(&mut self, _sim: &mut Sim, _dt: Duration) {}
}

pub struct RetainedRoot {
    key: String,
    generation: u64,
}

impl RetainedRoot {
    pub fn new() -> Self {
        Self {
            key: format!(
                "repame.retained_root.{}",
                repose_core::unique_component_id()
            ),
            generation: 0,
        }
    }

    pub fn call<F>(&mut self, sched: &mut repose_core::runtime::Scheduler, build: F) -> View
    where
        F: FnOnce(&mut repose_core::runtime::Scheduler) -> View,
    {
        let key = &self.key;
        let epoch = repose_core::timer::frame_count();
        let generation = self.generation;
        repose_core::scope!(key, sched, [epoch, generation], { build(sched) })
    }

    pub fn clear(&mut self) {
        self.generation = self.generation.wrapping_add(1);
    }
}

impl Default for RetainedRoot {
    fn default() -> Self {
        Self::new()
    }
}

/// Desktop entry point. Mounts `root` with title and size.
/// The root closure owns stepping and requests frames for continuity.
/// The closure receives the live [`Scheduler`] each frame, whose
/// `held_keys` / `window_focused` / `mouse_*` fields are the single
/// polled hardware snapshot (GML `keyboard_check` /
/// `mouse_check_button` parity).
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn run_desktop(
    title: &str,
    size: (u32, u32),
    mut root: impl FnMut(
        &mut repose_core::runtime::Scheduler,
        &repose_core::RenderContext,
    ) -> repose_core::View
    + 'static,
) -> Result<()> {
    let config = repose_app::AppConfig {
        window_title: title.to_string(),
        window_size: size,
        ..Default::default()
    };
    let mut retained = RetainedRoot::new();
    repose_platform::run_desktop_app_with_config(
        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
        config,
    )?;
    Ok(())
}

/// Web entry point. Mounts `root` with default options.
#[cfg(target_arch = "wasm32")]
pub fn run_web(
    mut root: impl FnMut(
        &mut repose_core::runtime::Scheduler,
        &repose_core::RenderContext,
    ) -> repose_core::View
    + 'static,
) -> Result<(), wasm_bindgen::JsValue> {
    let mut options = repose_platform::web::WebOptions::new(None);
    options.set_prevent_default(true);
    let mut retained = RetainedRoot::new();
    repose_platform::web::run_web_app(
        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
        options,
    )
}

/// Android entry point. Mounts `root` with default options.
#[cfg(target_os = "android")]
pub fn run_android(
    app: winit::platform::android::activity::AndroidApp,
    mut root: impl FnMut(
        &mut repose_core::runtime::Scheduler,
        &repose_core::RenderContext,
    ) -> repose_core::View
    + 'static,
) -> Result<()> {
    let mut retained = RetainedRoot::new();
    repose_platform::android::run_android_app_with_options(
        app,
        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
        repose_app::AndroidOptions::default(),
    )
}

/// Gamepad polling on the shared platform backend. Events feed
/// `ReposeRuntime::handle_gamepad` (UI nav) and `rt.gamepads` (gameplay).
pub struct GamepadPoller {
    backend: Option<Box<dyn GamepadBackend>>,
    connected: HashSet<u32>,
}

impl GamepadPoller {
    pub fn new() -> Self {
        Self {
            backend: create_backend().map(|b| Box::new(b) as Box<dyn GamepadBackend>),
            // Boot-plugged pads report on first poll; no press needed.
            connected: HashSet::new(),
        }
    }

    /// Drain hardware events since the last call. Tracks connection state
    /// so `connected_ids` stays live.
    pub fn poll(&mut self) -> Vec<GamepadEvent> {
        let Some(backend) = &mut self.backend else {
            return Vec::new();
        };
        let events = backend.poll();
        for ev in &events {
            match ev {
                GamepadEvent::Connected { id, .. } => {
                    self.connected.insert(id.0);
                }
                GamepadEvent::Disconnected { id } => {
                    self.connected.remove(&id.0);
                }
                GamepadEvent::Button { id, .. } | GamepadEvent::Axis { id, .. } => {
                    self.connected.insert(id.0);
                }
            }
        }
        events
    }

    pub fn connected_ids(&self) -> Vec<GamepadId> {
        let mut ids: Vec<GamepadId> = self.connected.iter().copied().map(GamepadId).collect();
        ids.sort_by_key(|id| id.0);
        ids
    }

    pub fn connected_count(&self) -> usize {
        self.connected.len()
    }
}

impl Default for GamepadPoller {
    fn default() -> Self {
        Self::new()
    }
}