repame-shell 0.3.9

Repose platform runner wrappers, sim stepping, viewport mount, and gamepad polling
Documentation
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;

use glam::Vec2;
use repame_input::{AimTracker, GamepadState, MouseState, StagedClick, TouchContact};
use repose_core::input::{Key, KeyEvent, KeyEventType, PhysicalKey, PointerButton};
use repose_core::runtime::Scheduler;

#[derive(Default)]
pub struct Staging {
    pub held: HashSet<PhysicalKey>,
    pub edges: Vec<PhysicalKey>,
    pub window_focused: bool,
    pub capture_armed: bool,
    pub clicks: Vec<StagedClick>,
    pub aim: AimTracker,
    pub mouse_edges: Vec<(PointerButton, bool)>,
    pub lmb_held: bool,
    pub rmb_held: bool,
    pub rmb_down_edge: bool,
    pub capture_pending_physical: Option<PhysicalKey>,
    pub capture_pending_key: Option<Key>,
    pub capture_pending_mouse: Option<bool>,
    pub touch_active: HashMap<u64, (Vec2, Vec2)>,
    pub touch_new: HashSet<u64>,
    pub pads: Vec<GamepadState>,
    pub pad_live: bool,
    pub view_width: f32,
}

impl Staging {
    pub fn shared() -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(Self {
            window_focused: true,
            view_width: 1280.0,
            ..Default::default()
        }))
    }

    pub fn handle_key(&mut self, ke: &KeyEvent) {
        let down = matches!(ke.event_type, KeyEventType::Down);
        if down && !ke.is_repeat {
            match &ke.key {
                Key::Backspace => {
                    self.capture_pending_key = Some(Key::Backspace);
                    return;
                }
                _ => {}
            }
            if self.capture_armed {
                if let Some(key) = ke.physical {
                    self.capture_pending_physical = Some(key);
                } else {
                    self.capture_pending_key = Some(ke.key.clone());
                }
                return;
            }
            if matches!(ke.key, Key::Escape | Key::Enter) {
                return;
            }
        }
        match &ke.key {
            Key::Escape | Key::Enter => {}
            Key::Character(_) => {
                if let Some(key) = ke.physical {
                    self.stage_physical(key, down, ke.is_repeat);
                }
            }
            Key::ShiftLeft => self.stage_code(PhysicalKey::ShiftLeft, down, ke.is_repeat),
            Key::ShiftRight => self.stage_code(PhysicalKey::ShiftRight, down, ke.is_repeat),
            Key::Space => self.stage_code(PhysicalKey::Space, down, ke.is_repeat),
            Key::Tab => self.stage_code(PhysicalKey::Tab, down, ke.is_repeat),
            Key::ArrowUp => self.stage_code(PhysicalKey::ArrowUp, down, ke.is_repeat),
            Key::ArrowDown => self.stage_code(PhysicalKey::ArrowDown, down, ke.is_repeat),
            Key::ArrowLeft => self.stage_code(PhysicalKey::ArrowLeft, down, ke.is_repeat),
            Key::ArrowRight => self.stage_code(PhysicalKey::ArrowRight, down, ke.is_repeat),
            _ => {}
        }
    }

    pub fn stage_physical(&mut self, key: PhysicalKey, down: bool, is_repeat: bool) {
        if down {
            if self.held.insert(key) && !is_repeat {
                self.edges.push(key);
            }
        } else {
            self.held.remove(&key);
        }
    }

    fn stage_code(&mut self, code: PhysicalKey, down: bool, is_repeat: bool) {
        self.stage_physical(code, down, is_repeat);
    }

    pub fn stage_physical_key(&mut self, key: PhysicalKey, down: bool) {
        self.stage_physical(key, down, false);
    }

    pub fn set_window_focused(&mut self, focused: bool) {
        self.window_focused = focused;
        if !focused {
            self.held.clear();
            self.edges.clear();
            self.capture_pending_physical = None;
            self.capture_pending_key = None;
            self.capture_pending_mouse = None;
            self.lmb_held = false;
            self.rmb_held = false;
            self.aim = AimTracker::default();
            self.clicks.clear();
            self.mouse_edges.clear();
            self.rmb_down_edge = false;
            self.touch_active.clear();
            self.touch_new.clear();
            self.pads.clear();
            self.pad_live = false;
        }
    }

    pub fn feed_polled(&mut self, sched: &Scheduler) {
        if !sched.window_focused {
            self.set_window_focused(false);
            return;
        }
        self.window_focused = true;
        // Identity-key repair: the polled set IS the reverse map here, so
        // missed releases drop without synthesizing press edges.
        self.held.retain(|key| sched.held_keys.contains(key));
        if !sched.mouse_primary {
            self.lmb_held = false;
        }
        if !sched.mouse_secondary {
            self.rmb_held = false;
        }
        if !sched.mouse_middle {
            self.mouse_edges
                .retain(|(button, _)| *button != PointerButton::Tertiary);
        }
        // The platform publishes the live contact
        // table (`sched.touch_points`, physical px) every event, and
        // `feed_polled` runs once per frame before the sampler.
        if sched.touch_points.is_empty() {
            if !self.touch_active.is_empty() {
                self.touch_active.clear();
                self.touch_new.clear();
            }
        } else {
            let d = repose_core::locals::effective_density_scale().max(1e-6);
            let live: std::collections::HashSet<u64> =
                sched.touch_points.iter().map(|(id, _, _)| *id).collect();
            self.touch_active
                .retain(|id, _| live.contains(id) || self.touch_new.contains(id));
            for (id, x, y) in sched.touch_points.iter() {
                let p = Vec2::new(*x, *y) / d;
                if let Some(contact) = self.touch_active.get_mut(id) {
                    contact.1 = p;
                } else {
                    self.touch_active.insert(*id, (p, p));
                    self.touch_new.insert(*id);
                }
            }
        }
    }

    pub fn pick_down(&mut self, button: PointerButton) {
        if self.capture_armed {
            self.capture_pending_mouse = Some(button == PointerButton::Primary);
            return;
        }
        self.mouse_edges.push((button, true));
        if button == PointerButton::Primary {
            self.lmb_held = true;
        } else {
            self.rmb_down_edge = true;
            self.rmb_held = true;
        }
    }

    pub fn pick_up(&mut self, button: PointerButton) {
        self.mouse_edges.push((button, false));
        if button == PointerButton::Primary {
            self.lmb_held = false;
        } else {
            self.rmb_held = false;
        }
    }

    pub fn take_capture_physical(&mut self) -> Option<PhysicalKey> {
        self.capture_pending_physical.take()
    }

    pub fn take_capture_key(&mut self) -> Option<Key> {
        self.capture_pending_key.take()
    }

    pub fn take_capture_mouse(&mut self) -> Option<bool> {
        self.capture_pending_mouse.take()
    }

    pub fn take_rmb_down(&mut self) -> bool {
        std::mem::replace(&mut self.rmb_down_edge, false)
    }

    pub fn stage_click(&mut self, world: Vec2, screen: [f32; 2], density: f32) {
        let d = density.max(1e-6);
        self.clicks.push(StagedClick {
            world,
            dp: [screen[0] / d, screen[1] / d],
        });
    }

    pub fn take_clicks(&mut self) -> Vec<StagedClick> {
        std::mem::take(&mut self.clicks)
    }

    pub fn take_edges(&mut self) -> Vec<PhysicalKey> {
        std::mem::take(&mut self.edges)
    }

    pub fn take_mouse_edges(&mut self) -> Vec<(PointerButton, bool)> {
        std::mem::take(&mut self.mouse_edges)
    }

    pub fn take_pads(&mut self) -> Vec<GamepadState> {
        self.pad_live = !self.pads.is_empty();
        std::mem::take(&mut self.pads)
    }

    pub fn stage_gamepad(&mut self, pad: GamepadState) {
        self.pads.push(pad);
        self.pad_live = true;
    }

    pub fn touch_down(&mut self, id: u64, screen_dp: Vec2) {
        self.touch_active.insert(id, (screen_dp, screen_dp));
        self.touch_new.insert(id);
    }

    pub fn touch_move(&mut self, id: u64, screen_dp: Vec2) {
        if let Some(contact) = self.touch_active.get_mut(&id) {
            contact.1 = screen_dp;
        }
    }

    pub fn touch_up(&mut self, id: u64) {
        self.touch_active.remove(&id);
        self.touch_new.remove(&id);
    }

    pub fn touch_contacts(&mut self) -> Vec<TouchContact> {
        let out: Vec<TouchContact> = self
            .touch_active
            .iter()
            .map(|(id, (start, pos))| TouchContact {
                id: *id,
                start: *start,
                pos: *pos,
                just_pressed: self.touch_new.contains(id),
            })
            .collect();
        self.touch_new.clear();
        out
    }

    pub fn cursor_move(&mut self, phys_px: Vec2) {
        self.aim.cursor_move(phys_px);
    }

    pub fn stage_hover(&mut self, world: Vec2, screen: [f32; 2]) {
        self.aim.stage_hover(world, screen);
    }

    pub fn mouse_state(&self, allow_fire: bool) -> MouseState {
        MouseState {
            left_held: self.lmb_held && allow_fire,
            left_pressed: !self.clicks.is_empty() && allow_fire,
            right_held: self.rmb_held,
            right_pressed: self.rmb_down_edge,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use repose_core::input::{Key, Modifiers};

    fn key(physical: PhysicalKey) -> KeyEvent {
        KeyEvent {
            key: Key::Character('x'),
            modifiers: Modifiers::default(),
            is_repeat: false,
            event_type: KeyEventType::Down,
            utf16_code_point: 0,
            physical: Some(physical),
        }
    }

    #[test]
    fn edges_dedupe_while_held() {
        let mut s = Staging::default();
        s.handle_key(&key(PhysicalKey::KeyW));
        s.handle_key(&key(PhysicalKey::KeyW));
        assert_eq!(s.take_edges().len(), 1);
    }

    #[test]
    fn focus_loss_clears_levels() {
        let mut s = Staging::default();
        s.handle_key(&key(PhysicalKey::KeyW));
        s.set_window_focused(false);
        assert!(s.held.is_empty());
    }

    #[test]
    fn capture_stashes_physical_and_skips_edges() {
        let mut s = Staging::default();
        s.capture_armed = true;
        s.handle_key(&key(PhysicalKey::KeyZ));
        assert_eq!(s.capture_pending_physical, Some(PhysicalKey::KeyZ));
        assert!(s.take_edges().is_empty());
    }

    #[test]
    fn capture_click_stashes_button_not_click_edge() {
        let mut s = Staging::default();
        s.capture_armed = true;
        s.pick_down(PointerButton::Primary);
        assert_eq!(s.capture_pending_mouse, Some(true));
        assert!(s.take_clicks().is_empty());
        assert!(!s.lmb_held);
    }
}