From 7bcf453425190ec1b4873d8ec3ab5e984d1e60fa Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Fri, 9 Apr 2021 08:50:41 -0700 Subject: [PATCH] Split `InputProcessor` to a new file. --- all-is-cubes/src/apps.rs | 377 +------------------------------- all-is-cubes/src/apps/input.rs | 382 +++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+), 374 deletions(-) create mode 100644 all-is-cubes/src/apps/input.rs diff --git a/all-is-cubes/src/apps.rs b/all-is-cubes/src/apps.rs index 28e1e8faf..15d598f8b 100644 --- a/all-is-cubes/src/apps.rs +++ b/all-is-cubes/src/apps.rs @@ -3,15 +3,10 @@ //! Components for "apps", or game clients: user interface and top-level state. -use cgmath::{EuclideanSpace as _, Point2, Vector2, Vector3, Zero as _}; -use std::collections::{HashMap, HashSet}; -use std::time::Duration; - -use crate::camera::{Camera, GraphicsOptions, Viewport}; +use crate::camera::{Camera, GraphicsOptions}; use crate::character::{cursor_raycast, Character, CharacterChange, Cursor}; use crate::content::UniverseTemplate; use crate::listen::{DirtyFlag, ListenableCell, ListenableSource, ListenerHelper as _}; -use crate::math::FreeCoordinate; use crate::space::Space; use crate::tools::ToolError; use crate::transactions::Transaction; @@ -170,371 +165,5 @@ impl AllIsCubesAppState { } } -/// Parse input events, particularly key-down/up pairs, into character control and such. -/// -/// This is designed to be a leaf of the dependency graph: it does not own or send -/// messages to any other elements of the application. Instead, the following steps -/// must occur in the given order. -/// -/// 1. The platform-specific code should call [`InputProcessor::key_down`] and such to -/// to provide input information. -/// 2. The game loop should call [`InputProcessor::apply_input`] to apply the effects -/// of input on the relevant [`Character`]. -/// 3. The game loop should call [`InputProcessor::step`] to apply the effects of time -/// on the input processor. -#[derive(Clone, Debug)] -pub struct InputProcessor { - /// All [`Key`]s currently pressed. - keys_held: HashSet, - /// As a special feature for supporting input without key-up events, stores all - /// keypresses arriving through [`Self::key_momentary`] and virtually holds them - /// for a short time. The value is the remaining time. - momentary_timeout: HashMap, - /// [`Key`]s with one-shot effects when pressed which need to be applied - /// once per press rather than while held. - command_buffer: Vec, - - /// Do we *want* pointer lock for mouselook? Controlled by UI. - mouselook_mode: bool, - /// Do we *have* pointer lock for mouselook? Reported by calling input implementation. - has_pointer_lock: bool, - - /// Net mouse movement since the last [`Self::apply_input`]. - mouselook_buffer: Vector2, - - /// Mouse position in NDC. None if out of bounds/lost focus. - mouse_ndc_position: Option>, - - /// Mouse position used for generating mouselook deltas. - /// [`None`] if games. - mouse_previous_pixel_position: Option>, -} - -impl InputProcessor { - #[allow(clippy::new_without_default)] // I expect it'll grow some parameters - pub fn new() -> Self { - Self { - keys_held: HashSet::new(), - momentary_timeout: HashMap::new(), - command_buffer: Vec::new(), - mouselook_mode: false, // TODO: might want a parameter - has_pointer_lock: false, - mouselook_buffer: Vector2::zero(), - mouse_ndc_position: Some(Point2::origin()), - mouse_previous_pixel_position: None, - } - } - - fn is_bound(key: Key) -> bool { - // Eventually we'll have actual configurable keybindings... - match key { - // Used in `InputProcessor::movement()`. - Key::Character('w') => true, - Key::Character('a') => true, - Key::Character('s') => true, - Key::Character('d') => true, - Key::Character('e') => true, - Key::Character('c') => true, - // Used in `InputProcessor::apply_input()`. - Key::Left => true, - Key::Right => true, - Key::Up => true, - Key::Down => true, - Key::Character(' ') => true, - Key::Character(d) if d.is_ascii_digit() => true, - Key::Character('l') => true, - _ => false, - } - } - - /// Returns true if the key should go in `command_buffer`. - fn is_command(key: Key) -> bool { - #[allow(clippy::match_like_matches_macro)] - match key { - Key::Character(d) if d.is_ascii_digit() => true, - Key::Character('l') => true, - _ => false, - } - } - - /// Handles incoming key-down events. Returns whether the key was unbound. - pub fn key_down(&mut self, key: Key) -> bool { - let bound = Self::is_bound(key); - if bound { - self.keys_held.insert(key); - if Self::is_command(key) { - self.command_buffer.push(key); - } - } - bound - } - - /// Handles incoming key-up events. - pub fn key_up(&mut self, key: Key) { - self.keys_held.remove(&key); - } - - /// Handles incoming key events in the case where key-up events are not available, - /// such that an assumption about equivalent press duration must be made. - pub fn key_momentary(&mut self, key: Key) -> bool { - self.momentary_timeout - .insert(key, Duration::from_millis(200)); - self.key_up(key); - self.key_down(key) - } - - /// Handles the keyboard focus being gained or lost. If the platform does not have - /// a concept of focus, you need not call this method, but may call it with `true`. - /// - /// `InputProcessor` will assume that if focus is lost, key-up events may be lost and - /// so currently held keys should stop taking effect. - pub fn key_focus(&mut self, has_focus: bool) { - if has_focus { - // Nothing to do. - } else { - self.keys_held.clear(); - self.momentary_timeout.clear(); - - self.mouselook_mode = false; - } - } - - /// True when the UI is in a state which _should_ have mouse pointer - /// lock/capture/disable. This is not the same as actually having it since the window - /// may lack focus, the application may lack permission, etc.; use - /// [`InputProcessor::has_pointer_lock`] to report that state. - pub fn wants_pointer_lock(&self) -> bool { - self.mouselook_mode - } - - /// Use this method to report whether mouse mouse pointer lock/capture/disable is - /// known to be successfully enabled, after [`InputProcessor::wants_pointer_lock`] - /// requests it or it is disabled for any reason. - pub fn has_pointer_lock(&mut self, value: bool) { - self.has_pointer_lock = value; - } - - /// Provide relative movement information for mouselook. - /// - /// This value is an accumulated displacement, not an angular velocity, so it is not - /// suitable for joystick-type input. - /// - /// Note that absolute cursor positions must be provided separately. - pub fn mouselook_delta(&mut self, delta: Vector2) { - // TODO: sensitivity option - if self.has_pointer_lock { - self.mouselook_buffer += delta * 0.2; - } - } - - /// Provide position of mouse pointer or other input device in normalized device - /// coordinates (range -1 to 1 upward and rightward). - /// [`None`] denotes the cursor being outside the viewport, and out-of-range - /// coordinates will be treated the same. - /// - /// Pixel coordinates may be converted to NDC using [`Viewport::normalize_nominal_point`] - /// or by using [`InputProcessor::mouse_pixel_position`]. - /// - /// If this is never called, the default value is (0, 0) which corresponds to the - /// center of the screen. - pub fn mouse_ndc_position(&mut self, position: Option>) { - self.mouse_ndc_position = position.filter(|p| p.x.abs() <= 1. && p.y.abs() <= 1.); - } - - /// Provide position of mouse pointer or other input device in pixel coordinates - /// framed by the given [`Viewport`]. - /// [`None`] denotes the cursor being outside the viewport, and out-of-range - /// coordinates will be treated the same. - /// - /// This is equivalent to converting the coordinates and calling - /// [`InputProcessor::mouse_ndc_position`]. - /// - /// If this is never called, the default value is (0, 0) which corresponds to the - /// center of the screen. - /// - /// TODO: this should take float input, probably - pub fn mouse_pixel_position( - &mut self, - viewport: Viewport, - position: Option>, - derive_movement: bool, - ) { - self.mouse_ndc_position( - position.map(|p| Point2::from_vec(viewport.normalize_nominal_point(p))), - ); - - if derive_movement { - if let (Some(p1), Some(p2)) = (self.mouse_previous_pixel_position, position) { - self.mouselook_delta((p2 - p1).map(FreeCoordinate::from)); - } - self.mouse_previous_pixel_position = position; - } else { - self.mouse_previous_pixel_position = None; - } - } - - /// Returns the character movement velocity that input is currently requesting. - pub fn movement(&self) -> Vector3 { - Vector3::new( - self.net_movement(Key::Character('a'), Key::Character('d')), - self.net_movement(Key::Character('c'), Key::Character('e')), - self.net_movement(Key::Character('w'), Key::Character('s')), - ) - } - - /// Advance time insofar as input interpretation is affected by time. - /// - /// This method should be called *after* [`apply_input`](Self::apply_input), when - /// applicable. - pub fn step(&mut self, timestep: Duration) { - let mut to_drop = Vec::new(); - for (key, duration) in self.momentary_timeout.iter_mut() { - if let Some(reduced) = duration.checked_sub(timestep) { - *duration = reduced; - } else { - to_drop.push(*key); - } - } - for key in to_drop.drain(..) { - self.momentary_timeout.remove(&key); - self.key_up(key); - } - - self.mouselook_buffer = Vector2::zero(); - } - - /// Applies the current input to the given [`Character`]. - pub fn apply_input(&mut self, character: &mut Character, timestep: Duration) { - let dt = timestep.as_secs_f64(); - let key_turning_step = 80.0 * dt; - - let movement = self.movement(); - character.set_velocity_input(movement); - - let turning = Vector2::new( - key_turning_step * self.net_movement(Key::Left, Key::Right) + self.mouselook_buffer.x, - key_turning_step * self.net_movement(Key::Up, Key::Down) + self.mouselook_buffer.y, - ); - character.body.yaw = (character.body.yaw + turning.x).rem_euclid(360.0); - character.body.pitch = (character.body.pitch + turning.y).min(90.0).max(-90.0); - - if self.keys_held.contains(&Key::Character(' ')) { - character.jump_if_able(); - } - - for key in self.command_buffer.drain(..) { - match key { - Key::Character('l') => { - self.mouselook_mode = !self.mouselook_mode; - if self.mouselook_mode { - // Clear delta tracking just in case - self.mouse_previous_pixel_position = None; - } - } - Key::Character(numeral) if numeral.is_digit(10) => { - let digit = numeral.to_digit(10).unwrap() as usize; - let slot = (digit + 9).rem_euclid(10); // wrap 0 to 9 - character.set_selected_slot(1, slot); - } - _ => {} - } - } - } - - /// Returns the position which should be used for click/cursor raycasting. - /// This is not necessarily equal to the tracked mouse position. - /// - /// Returns [`None`] if the mouse position is out of bounds, the window has lost - /// focus, or similar conditions under which no cursor should be shown. - pub fn cursor_ndc_position(&self) -> Option> { - if self.mouselook_mode { - Some(Point2::origin()) - } else { - self.mouse_ndc_position - } - } - - /// Computes the net effect of a pair of opposed inputs (e.g. "forward" and "back"). - fn net_movement(&self, negative: Key, positive: Key) -> FreeCoordinate { - match ( - self.keys_held.contains(&negative), - self.keys_held.contains(&positive), - ) { - (true, false) => -1.0, - (false, true) => 1.0, - _ => 0.0, - } - } -} - -/// A platform-neutral representation of keyboard keys for [`InputProcessor`]. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -#[non_exhaustive] -pub enum Key { - /// Letters should be lowercase. - Character(char), - /// Left arrow key. - Left, - /// Right arrow key. - Right, - /// Up arrow key. - Up, - /// Down arrow key. - Down, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn input_movement() { - let mut input = InputProcessor::new(); - assert_eq!(input.movement(), Vector3::new(0.0, 0.0, 0.0)); - input.key_down(Key::Character('d')); - assert_eq!(input.movement(), Vector3::new(1.0, 0.0, 0.0)); - input.key_down(Key::Character('a')); - assert_eq!(input.movement(), Vector3::new(0.0, 0.0, 0.0)); - input.key_up(Key::Character('d')); - assert_eq!(input.movement(), Vector3::new(-1.0, 0.0, 0.0)); - } - - #[test] - fn input_focus_lost_cancels_keys() { - let mut input = InputProcessor::new(); - assert_eq!(input.movement(), Vector3::zero()); - input.key_down(Key::Character('d')); - assert_eq!(input.movement(), Vector3::unit_x()); - input.key_focus(false); - assert_eq!(input.movement(), Vector3::zero()); // Lost focus, no movement. - - // Confirm that keys work again afterward. - input.key_focus(true); - assert_eq!(input.movement(), Vector3::zero()); - input.key_down(Key::Character('d')); - assert_eq!(input.movement(), Vector3::unit_x()); - // TODO: test (and handle) key events arriving while focus is lost, just in case. - } - - #[test] - fn input_slot_selection() { - // TODO: Awful lot of setup boilerplate... - let mut u = Universe::new(); - let space = u.insert_anonymous(Space::empty_positive(1, 1, 1)); - let character = u.insert_anonymous(Character::spawn_default(space.clone())); - let mut input = InputProcessor::new(); - - input.key_down(Key::Character('5')); - input.key_up(Key::Character('5')); - input.apply_input(&mut *character.borrow_mut(), Duration::from_secs(1)); - assert_eq!(character.borrow_mut().selected_slots()[1], 4); - - // Tenth slot - input.key_down(Key::Character('0')); - input.key_up(Key::Character('0')); - input.apply_input(&mut *character.borrow_mut(), Duration::from_secs(1)); - assert_eq!(character.borrow_mut().selected_slots()[1], 9); - } - - // TODO: test jump and flying logic -} +mod input; +pub use input::*; diff --git a/all-is-cubes/src/apps/input.rs b/all-is-cubes/src/apps/input.rs new file mode 100644 index 000000000..d84435d23 --- /dev/null +++ b/all-is-cubes/src/apps/input.rs @@ -0,0 +1,382 @@ +// Copyright 2020-2021 Kevin Reid under the terms of the MIT License as detailed +// in the accompanying file README.md or . + +use cgmath::{EuclideanSpace as _, Point2, Vector2, Vector3, Zero as _}; +use std::collections::{HashMap, HashSet}; +use std::time::Duration; + +use crate::camera::Viewport; +use crate::character::Character; +use crate::math::FreeCoordinate; + +/// Parse input events, particularly key-down/up pairs, into character control and such. +/// +/// This is designed to be a leaf of the dependency graph: it does not own or send +/// messages to any other elements of the application. Instead, the following steps +/// must occur in the given order. +/// +/// 1. The platform-specific code should call [`InputProcessor::key_down`] and such to +/// to provide input information. +/// 2. The game loop should call [`InputProcessor::apply_input`] to apply the effects +/// of input on the relevant [`Character`]. +/// 3. The game loop should call [`InputProcessor::step`] to apply the effects of time +/// on the input processor. +#[derive(Clone, Debug)] +pub struct InputProcessor { + /// All [`Key`]s currently pressed. + keys_held: HashSet, + /// As a special feature for supporting input without key-up events, stores all + /// keypresses arriving through [`Self::key_momentary`] and virtually holds them + /// for a short time. The value is the remaining time. + momentary_timeout: HashMap, + /// [`Key`]s with one-shot effects when pressed which need to be applied + /// once per press rather than while held. + command_buffer: Vec, + + /// Do we *want* pointer lock for mouselook? Controlled by UI. + // TODO: Stop making this public directly + pub(super) mouselook_mode: bool, + /// Do we *have* pointer lock for mouselook? Reported by calling input implementation. + has_pointer_lock: bool, + + /// Net mouse movement since the last [`Self::apply_input`]. + mouselook_buffer: Vector2, + + /// Mouse position in NDC. None if out of bounds/lost focus. + mouse_ndc_position: Option>, + + /// Mouse position used for generating mouselook deltas. + /// [`None`] if games. + mouse_previous_pixel_position: Option>, +} + +impl InputProcessor { + #[allow(clippy::new_without_default)] // I expect it'll grow some parameters + pub fn new() -> Self { + Self { + keys_held: HashSet::new(), + momentary_timeout: HashMap::new(), + command_buffer: Vec::new(), + mouselook_mode: false, // TODO: might want a parameter + has_pointer_lock: false, + mouselook_buffer: Vector2::zero(), + mouse_ndc_position: Some(Point2::origin()), + mouse_previous_pixel_position: None, + } + } + + fn is_bound(key: Key) -> bool { + // Eventually we'll have actual configurable keybindings... + match key { + // Used in `InputProcessor::movement()`. + Key::Character('w') => true, + Key::Character('a') => true, + Key::Character('s') => true, + Key::Character('d') => true, + Key::Character('e') => true, + Key::Character('c') => true, + // Used in `InputProcessor::apply_input()`. + Key::Left => true, + Key::Right => true, + Key::Up => true, + Key::Down => true, + Key::Character(' ') => true, + Key::Character(d) if d.is_ascii_digit() => true, + Key::Character('l') => true, + _ => false, + } + } + + /// Returns true if the key should go in `command_buffer`. + fn is_command(key: Key) -> bool { + #[allow(clippy::match_like_matches_macro)] + match key { + Key::Character(d) if d.is_ascii_digit() => true, + Key::Character('l') => true, + _ => false, + } + } + + /// Handles incoming key-down events. Returns whether the key was unbound. + pub fn key_down(&mut self, key: Key) -> bool { + let bound = Self::is_bound(key); + if bound { + self.keys_held.insert(key); + if Self::is_command(key) { + self.command_buffer.push(key); + } + } + bound + } + + /// Handles incoming key-up events. + pub fn key_up(&mut self, key: Key) { + self.keys_held.remove(&key); + } + + /// Handles incoming key events in the case where key-up events are not available, + /// such that an assumption about equivalent press duration must be made. + pub fn key_momentary(&mut self, key: Key) -> bool { + self.momentary_timeout + .insert(key, Duration::from_millis(200)); + self.key_up(key); + self.key_down(key) + } + + /// Handles the keyboard focus being gained or lost. If the platform does not have + /// a concept of focus, you need not call this method, but may call it with `true`. + /// + /// `InputProcessor` will assume that if focus is lost, key-up events may be lost and + /// so currently held keys should stop taking effect. + pub fn key_focus(&mut self, has_focus: bool) { + if has_focus { + // Nothing to do. + } else { + self.keys_held.clear(); + self.momentary_timeout.clear(); + + self.mouselook_mode = false; + } + } + + /// True when the UI is in a state which _should_ have mouse pointer + /// lock/capture/disable. This is not the same as actually having it since the window + /// may lack focus, the application may lack permission, etc.; use + /// [`InputProcessor::has_pointer_lock`] to report that state. + pub fn wants_pointer_lock(&self) -> bool { + self.mouselook_mode + } + + /// Use this method to report whether mouse mouse pointer lock/capture/disable is + /// known to be successfully enabled, after [`InputProcessor::wants_pointer_lock`] + /// requests it or it is disabled for any reason. + pub fn has_pointer_lock(&mut self, value: bool) { + self.has_pointer_lock = value; + } + + /// Provide relative movement information for mouselook. + /// + /// This value is an accumulated displacement, not an angular velocity, so it is not + /// suitable for joystick-type input. + /// + /// Note that absolute cursor positions must be provided separately. + pub fn mouselook_delta(&mut self, delta: Vector2) { + // TODO: sensitivity option + if self.has_pointer_lock { + self.mouselook_buffer += delta * 0.2; + } + } + + /// Provide position of mouse pointer or other input device in normalized device + /// coordinates (range -1 to 1 upward and rightward). + /// [`None`] denotes the cursor being outside the viewport, and out-of-range + /// coordinates will be treated the same. + /// + /// Pixel coordinates may be converted to NDC using [`Viewport::normalize_nominal_point`] + /// or by using [`InputProcessor::mouse_pixel_position`]. + /// + /// If this is never called, the default value is (0, 0) which corresponds to the + /// center of the screen. + pub fn mouse_ndc_position(&mut self, position: Option>) { + self.mouse_ndc_position = position.filter(|p| p.x.abs() <= 1. && p.y.abs() <= 1.); + } + + /// Provide position of mouse pointer or other input device in pixel coordinates + /// framed by the given [`Viewport`]. + /// [`None`] denotes the cursor being outside the viewport, and out-of-range + /// coordinates will be treated the same. + /// + /// This is equivalent to converting the coordinates and calling + /// [`InputProcessor::mouse_ndc_position`]. + /// + /// If this is never called, the default value is (0, 0) which corresponds to the + /// center of the screen. + /// + /// TODO: this should take float input, probably + pub fn mouse_pixel_position( + &mut self, + viewport: Viewport, + position: Option>, + derive_movement: bool, + ) { + self.mouse_ndc_position( + position.map(|p| Point2::from_vec(viewport.normalize_nominal_point(p))), + ); + + if derive_movement { + if let (Some(p1), Some(p2)) = (self.mouse_previous_pixel_position, position) { + self.mouselook_delta((p2 - p1).map(FreeCoordinate::from)); + } + self.mouse_previous_pixel_position = position; + } else { + self.mouse_previous_pixel_position = None; + } + } + + /// Returns the character movement velocity that input is currently requesting. + pub fn movement(&self) -> Vector3 { + Vector3::new( + self.net_movement(Key::Character('a'), Key::Character('d')), + self.net_movement(Key::Character('c'), Key::Character('e')), + self.net_movement(Key::Character('w'), Key::Character('s')), + ) + } + + /// Advance time insofar as input interpretation is affected by time. + /// + /// This method should be called *after* [`apply_input`](Self::apply_input), when + /// applicable. + pub fn step(&mut self, timestep: Duration) { + let mut to_drop = Vec::new(); + for (key, duration) in self.momentary_timeout.iter_mut() { + if let Some(reduced) = duration.checked_sub(timestep) { + *duration = reduced; + } else { + to_drop.push(*key); + } + } + for key in to_drop.drain(..) { + self.momentary_timeout.remove(&key); + self.key_up(key); + } + + self.mouselook_buffer = Vector2::zero(); + } + + /// Applies the current input to the given [`Character`]. + pub fn apply_input(&mut self, character: &mut Character, timestep: Duration) { + let dt = timestep.as_secs_f64(); + let key_turning_step = 80.0 * dt; + + let movement = self.movement(); + character.set_velocity_input(movement); + + let turning = Vector2::new( + key_turning_step * self.net_movement(Key::Left, Key::Right) + self.mouselook_buffer.x, + key_turning_step * self.net_movement(Key::Up, Key::Down) + self.mouselook_buffer.y, + ); + character.body.yaw = (character.body.yaw + turning.x).rem_euclid(360.0); + character.body.pitch = (character.body.pitch + turning.y).min(90.0).max(-90.0); + + if self.keys_held.contains(&Key::Character(' ')) { + character.jump_if_able(); + } + + for key in self.command_buffer.drain(..) { + match key { + Key::Character('l') => { + self.mouselook_mode = !self.mouselook_mode; + if self.mouselook_mode { + // Clear delta tracking just in case + self.mouse_previous_pixel_position = None; + } + } + Key::Character(numeral) if numeral.is_digit(10) => { + let digit = numeral.to_digit(10).unwrap() as usize; + let slot = (digit + 9).rem_euclid(10); // wrap 0 to 9 + character.set_selected_slot(1, slot); + } + _ => {} + } + } + } + + /// Returns the position which should be used for click/cursor raycasting. + /// This is not necessarily equal to the tracked mouse position. + /// + /// Returns [`None`] if the mouse position is out of bounds, the window has lost + /// focus, or similar conditions under which no cursor should be shown. + pub fn cursor_ndc_position(&self) -> Option> { + if self.mouselook_mode { + Some(Point2::origin()) + } else { + self.mouse_ndc_position + } + } + + /// Computes the net effect of a pair of opposed inputs (e.g. "forward" and "back"). + fn net_movement(&self, negative: Key, positive: Key) -> FreeCoordinate { + match ( + self.keys_held.contains(&negative), + self.keys_held.contains(&positive), + ) { + (true, false) => -1.0, + (false, true) => 1.0, + _ => 0.0, + } + } +} + +/// A platform-neutral representation of keyboard keys for [`InputProcessor`]. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum Key { + /// Letters should be lowercase. + Character(char), + /// Left arrow key. + Left, + /// Right arrow key. + Right, + /// Up arrow key. + Up, + /// Down arrow key. + Down, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::space::Space; + use crate::universe::Universe; + + #[test] + fn movement() { + let mut input = InputProcessor::new(); + assert_eq!(input.movement(), Vector3::new(0.0, 0.0, 0.0)); + input.key_down(Key::Character('d')); + assert_eq!(input.movement(), Vector3::new(1.0, 0.0, 0.0)); + input.key_down(Key::Character('a')); + assert_eq!(input.movement(), Vector3::new(0.0, 0.0, 0.0)); + input.key_up(Key::Character('d')); + assert_eq!(input.movement(), Vector3::new(-1.0, 0.0, 0.0)); + } + + #[test] + fn focus_lost_cancels_keys() { + let mut input = InputProcessor::new(); + assert_eq!(input.movement(), Vector3::zero()); + input.key_down(Key::Character('d')); + assert_eq!(input.movement(), Vector3::unit_x()); + input.key_focus(false); + assert_eq!(input.movement(), Vector3::zero()); // Lost focus, no movement. + + // Confirm that keys work again afterward. + input.key_focus(true); + assert_eq!(input.movement(), Vector3::zero()); + input.key_down(Key::Character('d')); + assert_eq!(input.movement(), Vector3::unit_x()); + // TODO: test (and handle) key events arriving while focus is lost, just in case. + } + + #[test] + fn slot_selection() { + // TODO: Awful lot of setup boilerplate... + let mut u = Universe::new(); + let space = u.insert_anonymous(Space::empty_positive(1, 1, 1)); + let character = u.insert_anonymous(Character::spawn_default(space.clone())); + let mut input = InputProcessor::new(); + + input.key_down(Key::Character('5')); + input.key_up(Key::Character('5')); + input.apply_input(&mut *character.borrow_mut(), Duration::from_secs(1)); + assert_eq!(character.borrow_mut().selected_slots()[1], 4); + + // Tenth slot + input.key_down(Key::Character('0')); + input.key_up(Key::Character('0')); + input.apply_input(&mut *character.borrow_mut(), Duration::from_secs(1)); + assert_eq!(character.borrow_mut().selected_slots()[1], 9); + } + + // TODO: test jump and flying logic +}