mod active_pad;
pub mod binding_asset;
pub mod binding_text;
mod context;
mod device;
mod eight_way;
mod emit;
#[cfg(any(target_os = "linux", test))]
pub mod evdev;
#[cfg(test)]
mod ffi_layout;
#[cfg(any(target_arch = "wasm32", target_os = "macos", test))]
mod float_axis;
#[cfg(any(target_os = "macos", test))]
pub mod game_controller;
mod gamepad;
pub mod hint;
pub mod list;
mod overrides;
mod patterns;
#[cfg(any(target_arch = "wasm32", target_os = "macos", test))]
mod product_name;
mod repeat;
pub mod text;
pub mod trace;
pub mod ui;
#[cfg(any(target_os = "linux", target_arch = "wasm32", test))]
mod usb;
#[cfg(any(target_arch = "wasm32", test))]
pub mod web_gamepad;
#[cfg(any(windows, test))]
pub mod xinput;
pub use active_pad::{ActivePad, StickUpdate};
pub use binding_asset::{AssetRefusal, AssetWriteError, BindingAssetError};
pub use binding_text::BindingParseError;
pub use context::{GAMEPLAY_CONTEXT, GLOBAL_CONTEXT};
pub use device::Device;
pub use eight_way::{EIGHT_WAY_SECTOR, eight_way};
pub use emit::Pattern;
pub use gamepad::{
GamepadEvent, GamepadId, GamepadSnapshot, PAD_ACTIVITY_THRESHOLD, PadAxis, PadButton,
PadButtons, PadKind, Stick, Trigger, radial_deadzone,
};
pub use hint::{DefaultLabels, Hint, HintLabels};
pub use overrides::{ActionOverride, OverrideRefusal};
pub use patterns::{DOUBLE_TAP_WINDOW, DoubleTap, HOLD_TIME, Hold, TAP_TIME, Tap};
pub use repeat::{Cardinal, REPEAT_DELAY, REPEAT_INTERVAL, Repeat};
pub use trace::{Outcome, RESOLUTION_TRACE_CAP, TraceEntry, TracedInput, TracedRead};
use context::{Routes, Suppressed, View};
use crcbl_core::input::{KeyCode, PointerButton};
use patterns::PatternState;
use repeat::RepeatState;
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
type Pads = BTreeMap<GamepadId, GamepadSnapshot>;
type HeldKeys = HashMap<KeyCode, u64>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ButtonState {
Pressed,
Released,
Held {
duration: f32,
},
}
#[derive(Debug, Clone)]
pub struct ButtonAction {
pub state: ButtonState,
pub just_pressed: bool,
pub just_released: bool,
}
impl ButtonAction {
fn reset_edges(&mut self) {
self.just_pressed = false;
self.just_released = false;
}
}
impl Default for ButtonAction {
fn default() -> Self {
Self {
state: ButtonState::Released,
just_pressed: false,
just_released: false,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Axis1Action {
pub value: f32,
pub pointer_moved: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Axis2Action {
pub x: f32,
pub y: f32,
}
#[derive(Debug, Clone)]
pub enum ActionValue {
Button(ButtonAction),
Axis1(Axis1Action),
Axis2(Axis2Action),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PointerAxis {
X,
Y,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Modifier {
Shift,
Control,
Alt,
Super,
}
impl Modifier {
#[must_use]
pub const fn keys(self) -> [KeyCode; 2] {
match self {
Self::Shift => [KeyCode::ShiftLeft, KeyCode::ShiftRight],
Self::Control => [KeyCode::ControlLeft, KeyCode::ControlRight],
Self::Alt => [KeyCode::AltLeft, KeyCode::AltRight],
Self::Super => [KeyCode::SuperLeft, KeyCode::SuperRight],
}
}
#[must_use]
pub const fn of(key: KeyCode) -> Option<Self> {
match key {
KeyCode::ShiftLeft | KeyCode::ShiftRight => Some(Self::Shift),
KeyCode::ControlLeft | KeyCode::ControlRight => Some(Self::Control),
KeyCode::AltLeft | KeyCode::AltRight => Some(Self::Alt),
KeyCode::SuperLeft | KeyCode::SuperRight => Some(Self::Super),
_ => None,
}
}
fn held(self, held: &HeldKeys) -> bool {
self.keys().iter().any(|key| held.contains_key(key))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Binding {
Key(KeyCode),
MouseButton(PointerButton),
MouseMotion,
MouseScroll,
PointerPosition {
axis: PointerAxis,
},
KeyAxis {
negative: KeyCode,
positive: KeyCode,
},
Chord {
modifier: Modifier,
key: KeyCode,
},
ButtonChord {
modifier: Modifier,
button: PointerButton,
},
ScrollChord {
held: KeyCode,
},
Virtual(String),
Wasd {
up: KeyCode,
down: KeyCode,
left: KeyCode,
right: KeyCode,
},
PadButton(PadButton),
PadDpad,
PadStick {
stick: Stick,
deadzone: f32,
},
PadChord {
modifier: PadButton,
button: PadButton,
},
PadTrigger {
trigger: Trigger,
threshold: f32,
},
}
impl Binding {
pub fn visit_keys(&self, mut visit: impl FnMut(KeyCode)) {
match self {
Self::Key(key) | Self::Chord { key, .. } => visit(*key),
Self::KeyAxis { negative, positive } => {
visit(*negative);
visit(*positive);
}
Self::Wasd {
up,
down,
left,
right,
} => {
visit(*up);
visit(*down);
visit(*left);
visit(*right);
}
Self::MouseButton(_)
| Self::ButtonChord { .. }
| Self::MouseMotion
| Self::MouseScroll
| Self::ScrollChord { .. }
| Self::PointerPosition { .. }
| Self::Virtual(_)
| Self::PadButton(_)
| Self::PadChord { .. }
| Self::PadDpad
| Self::PadStick { .. }
| Self::PadTrigger { .. } => {}
}
}
const fn reads_wheel(&self) -> bool {
matches!(self, Self::MouseScroll | Self::ScrollChord { .. })
}
const fn mouse_button(&self) -> Option<PointerButton> {
match *self {
Self::MouseButton(button) | Self::ButtonChord { button, .. } => Some(button),
_ => None,
}
}
#[must_use]
pub const fn reads_gamepad(&self) -> bool {
matches!(
self,
Self::PadButton(_)
| Self::PadChord { .. }
| Self::PadDpad
| Self::PadStick { .. }
| Self::PadTrigger { .. }
)
}
fn deadzone_is_valid(&self) -> bool {
match *self {
Self::PadStick { deadzone, .. } => gamepad::valid_deadzone(deadzone),
Self::PadTrigger { threshold, .. } => gamepad::valid_deadzone(threshold),
_ => true,
}
}
fn reads_keyboard(&self) -> bool {
let mut any = false;
self.visit_keys(|_| any = true);
any
}
#[must_use]
pub fn owns_key(&self, key: KeyCode) -> bool {
let mut owns = false;
self.visit_keys(|owned| owns |= owned == key);
owns
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActionKind {
Button,
Axis1,
Axis2,
}
#[derive(Debug, Clone)]
pub struct ActionDecl {
pub name: String,
pub kind: ActionKind,
pub bindings: Vec<Binding>,
}
#[derive(Debug, Clone)]
struct ActionSlot {
decl: ActionDecl,
defaults: Vec<Binding>,
context: usize,
repeat: Option<RepeatState>,
patterns: PatternState,
emitted: bool,
value: ActionValue,
active: bool,
hold_start: Option<f64>,
enabled: bool,
pad_chords_outrank: bool,
}
impl ActionSlot {
fn new(decl: ActionDecl, context: usize) -> Self {
let value = match decl.kind {
ActionKind::Button => ActionValue::Button(ButtonAction::default()),
ActionKind::Axis1 => ActionValue::Axis1(Axis1Action::default()),
ActionKind::Axis2 => ActionValue::Axis2(Axis2Action::default()),
};
Self {
defaults: decl.bindings.clone(),
decl,
context,
repeat: None,
patterns: PatternState::default(),
emitted: false,
value,
active: false,
hold_start: None,
enabled: true,
pad_chords_outrank: false,
}
}
fn kind(&self) -> ActionKind {
self.decl.kind
}
fn reset(&mut self) {
self.active = false;
self.hold_start = None;
if let Some(repeat) = &mut self.repeat {
repeat.reset();
}
self.patterns.reset();
self.emitted = false;
match &mut self.value {
ActionValue::Button(a) => {
a.state = ButtonState::Released;
a.just_pressed = false;
a.just_released = false;
}
ActionValue::Axis1(a) => {
a.value = 0.0;
a.pointer_moved = false;
}
ActionValue::Axis2(a) => {
a.x = 0.0;
a.y = 0.0;
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionMapError {
DuplicateName(String),
UnknownAction(String),
UnknownContext(String),
ContextAlreadyActive(String),
ContextNotOnTop(String),
InvalidDeadzone(String),
NotAButton(String),
EmitsItself(String),
}
impl std::fmt::Display for ActionMapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DuplicateName(name) => write!(f, "duplicate action name: {name}"),
Self::UnknownAction(name) => write!(f, "no such action: {name}"),
Self::UnknownContext(name) => write!(f, "no such context: {name}"),
Self::ContextAlreadyActive(name) => write!(f, "context already active: {name}"),
Self::ContextNotOnTop(name) => {
write!(f, "context is not the topmost pushed one: {name}")
}
Self::InvalidDeadzone(name) => {
write!(f, "dead zone or threshold not in 0..1: {name}")
}
Self::NotAButton(name) => {
write!(f, "a pattern can emit only a button action: {name}")
}
Self::EmitsItself(name) => {
write!(f, "a pattern cannot emit the action it is on: {name}")
}
}
}
}
impl std::error::Error for ActionMapError {}
pub struct ActionMap {
slots: Vec<ActionSlot>,
name_to_idx: HashMap<String, usize>,
contexts: Vec<String>,
stack: Vec<usize>,
modal: Vec<usize>,
routes: Routes,
suppressed: Suppressed,
devices: Vec<Device>,
last_pad_kind: Option<PadKind>,
device_changed: bool,
trace: Option<VecDeque<TraceEntry>>,
held_keys: HeldKeys,
key_presses: u64,
held_buttons: HashSet<PointerButton>,
held_controls: HashSet<String>,
control_sticks: HashMap<String, (f32, f32)>,
pads: Pads,
held_pad_buttons: PadButtons,
mouse_delta: (f32, f32),
scroll_delta: (f32, f32),
pointer: Option<(f32, f32)>,
pending_emits: Vec<usize>,
elapsed: f64,
}
impl ActionMap {
#[must_use]
pub fn new() -> Self {
Self {
slots: Vec::new(),
name_to_idx: HashMap::new(),
contexts: vec![GAMEPLAY_CONTEXT.to_owned(), GLOBAL_CONTEXT.to_owned()],
stack: vec![0],
modal: Vec::new(),
routes: Routes::default(),
suppressed: Suppressed::default(),
devices: Vec::new(),
last_pad_kind: None,
device_changed: false,
trace: None,
held_keys: HeldKeys::new(),
key_presses: 0,
held_buttons: HashSet::new(),
held_controls: HashSet::new(),
control_sticks: HashMap::new(),
pads: Pads::new(),
held_pad_buttons: PadButtons::EMPTY,
mouse_delta: (0.0, 0.0),
scroll_delta: (0.0, 0.0),
pointer: None,
pending_emits: Vec::new(),
elapsed: 0.0,
}
}
pub fn declare(&mut self, decl: ActionDecl) {
if let Err(error) = self.try_declare(decl) {
panic!("{error}");
}
}
pub fn try_declare(&mut self, decl: ActionDecl) -> Result<(), ActionMapError> {
self.try_declare_in(GAMEPLAY_CONTEXT, decl)
}
pub fn declare_in(&mut self, context: &str, decl: ActionDecl) {
if let Err(error) = self.try_declare_in(context, decl) {
panic!("{error}");
}
}
pub fn try_declare_in(
&mut self,
context: &str,
decl: ActionDecl,
) -> Result<(), ActionMapError> {
if self.name_to_idx.contains_key(&decl.name) {
return Err(ActionMapError::DuplicateName(decl.name));
}
if !decl.bindings.iter().all(Binding::deadzone_is_valid) {
return Err(ActionMapError::InvalidDeadzone(decl.name));
}
let context = match self.contexts.iter().position(|name| name == context) {
Some(index) => index,
None => {
self.contexts.push(context.to_owned());
self.contexts.len() - 1
}
};
let idx = self.slots.len();
self.name_to_idx.insert(decl.name.clone(), idx);
self.slots.push(ActionSlot::new(decl, context));
self.reroute();
Ok(())
}
pub fn rebind(&mut self, name: &str, bindings: Vec<Binding>) -> Result<(), ActionMapError> {
let Some(&idx) = self.name_to_idx.get(name) else {
return Err(ActionMapError::UnknownAction(name.to_owned()));
};
if !bindings.iter().all(Binding::deadzone_is_valid) {
return Err(ActionMapError::InvalidDeadzone(name.to_owned()));
}
let slot = &mut self.slots[idx];
slot.decl.bindings = bindings;
slot.reset();
self.reroute();
Ok(())
}
#[must_use]
pub fn bindings(&self, name: &str) -> Option<&[Binding]> {
let &idx = self.name_to_idx.get(name)?;
Some(&self.slots[idx].decl.bindings)
}
#[must_use]
pub fn is_enabled(&self, name: &str) -> Option<bool> {
let &idx = self.name_to_idx.get(name)?;
Some(self.slots[idx].enabled)
}
pub fn set_enabled(&mut self, name: &str, enabled: bool) {
let Some(&idx) = self.name_to_idx.get(name) else {
return;
};
let slot = &mut self.slots[idx];
let changed = slot.enabled != enabled;
slot.enabled = enabled;
if !enabled {
slot.reset();
}
let scroll_chord = slot
.decl
.bindings
.iter()
.any(|binding| matches!(binding, Binding::ScrollChord { .. }));
if changed && scroll_chord {
self.reroute();
}
}
pub fn key_event(&mut self, key: KeyCode, pressed: bool) {
let fresh = pressed && !self.held_keys.contains_key(&key);
if pressed {
if fresh {
self.key_presses += 1;
self.held_keys.insert(key, self.key_presses);
}
self.spoke(Device::Keyboard);
} else {
self.held_keys.remove(&key);
self.suppressed.keys.remove(&key);
}
let wheel = self.routes.is_scroll_chord_key(key);
if Modifier::of(key).is_some() {
self.resolve_matching(|b| {
b.reads_keyboard() || b.mouse_button().is_some() || (wheel && b.reads_wheel())
});
} else {
self.resolve_matching(|b| b.owns_key(key) || (wheel && b.reads_wheel()));
}
if fresh {
self.traced(|| TracedInput::Key(key));
}
}
pub fn mouse_button(&mut self, button: PointerButton, pressed: bool) {
let mut fresh = false;
if pressed {
fresh = self.held_buttons.insert(button);
self.spoke(Device::Pointer);
} else {
self.held_buttons.remove(&button);
self.suppressed.buttons.remove(&button);
}
self.resolve_matching(|b| b.mouse_button() == Some(button));
if fresh {
self.traced(|| TracedInput::MouseButton(button));
}
}
pub fn mouse_motion(&mut self, dx: f32, dy: f32) {
if !dx.is_finite() || !dy.is_finite() {
return;
}
if dx != 0.0 || dy != 0.0 {
self.spoke(Device::Pointer);
}
self.mouse_delta.0 += dx;
self.mouse_delta.1 += dy;
self.resolve_matching(|b| matches!(b, Binding::MouseMotion));
}
pub fn mouse_scroll(&mut self, dx: f32, dy: f32) {
if !dx.is_finite() || !dy.is_finite() {
return;
}
let turned = dx != 0.0 || dy != 0.0;
if turned {
self.spoke(Device::Pointer);
}
self.scroll_delta.0 += dx;
self.scroll_delta.1 += dy;
self.resolve_matching(Binding::reads_wheel);
if turned {
self.traced(|| TracedInput::Wheel);
}
}
pub fn pointer_position(&mut self, x: f32, y: f32) {
if !x.is_finite() || !y.is_finite() {
return;
}
let moved = Some((x, y));
if self.pointer == moved {
return;
}
self.pointer = moved;
self.spoke(Device::Pointer);
for i in 0..self.slots.len() {
let slot = &self.slots[i];
let drives = slot
.decl
.bindings
.iter()
.any(|b| matches!(b, Binding::PointerPosition { .. }));
if !self.is_live(i) || !drives || self.routes.pointer != Some(slot.context) {
continue;
}
self.resolve_one(i);
if let ActionValue::Axis1(axis) = &mut self.slots[i].value {
axis.pointer_moved = true;
}
}
}
pub fn virtual_button(&mut self, control: &str, pressed: bool) {
let fresh = pressed && !self.held_controls.contains(control);
if pressed {
if fresh {
self.held_controls.insert(control.to_owned());
}
self.spoke(Device::Touch);
} else {
self.held_controls.remove(control);
self.suppressed.controls.remove(control);
}
self.resolve_matching(|b| matches!(b, Binding::Virtual(id) if id == control));
if fresh {
self.traced(|| TracedInput::Control(control.to_owned()));
}
}
pub fn virtual_stick(&mut self, control: &str, x: f32, y: f32) {
if !x.is_finite() || !y.is_finite() {
return;
}
let deflected = x != 0.0 || y != 0.0;
if deflected {
self.spoke(Device::Touch);
}
let before = if let Some(held) = self.control_sticks.get_mut(control) {
std::mem::replace(held, (x, y))
} else {
self.control_sticks.insert(control.to_owned(), (x, y));
(0.0, 0.0)
};
if !deflected {
self.suppressed.control_sticks.remove(control);
}
self.resolve_matching(|b| matches!(b, Binding::Virtual(id) if id == control));
if deflected && before == (0.0, 0.0) {
self.traced(|| TracedInput::ControlStick(control.to_owned()));
}
}
pub fn begin_tick(&mut self, dt: f32) {
if dt.is_finite() && dt >= 0.0 {
self.elapsed += f64::from(dt);
}
self.mouse_delta = (0.0, 0.0);
self.scroll_delta = (0.0, 0.0);
self.device_changed = false;
for slot in &mut self.slots {
slot.emitted = false;
}
for i in 0..self.slots.len() {
if !self.is_live(i) {
continue;
}
if let Some(repeat) = &mut self.slots[i].repeat {
repeat.fired = false;
}
self.slots[i].patterns.clear_fired();
match &mut self.slots[i].value {
ActionValue::Button(a) => a.reset_edges(),
ActionValue::Axis1(a) => a.pointer_moved = false,
ActionValue::Axis2(_) => {}
}
self.resolve_slot(i);
}
self.drain_emits();
}
#[must_use]
pub fn action(&self, name: &str) -> Option<&ActionValue> {
let &idx = self.name_to_idx.get(name)?;
Some(&self.slots[idx].value)
}
#[must_use]
pub fn action_mut(&mut self, name: &str) -> Option<&mut ActionValue> {
let &idx = self.name_to_idx.get(name)?;
Some(&mut self.slots[idx].value)
}
pub fn action_names(&self) -> impl Iterator<Item = &str> {
self.slots.iter().map(|s| s.decl.name.as_str())
}
#[must_use]
pub fn button_held(&self, name: &str) -> bool {
self.button(name).is_some_and(|button| {
matches!(
button.state,
ButtonState::Pressed | ButtonState::Held { .. }
)
})
}
#[must_use]
pub fn just_pressed(&self, name: &str) -> bool {
self.button(name).is_some_and(|button| button.just_pressed)
}
#[must_use]
pub fn just_released(&self, name: &str) -> bool {
self.button(name).is_some_and(|button| button.just_released)
}
#[must_use]
pub fn axis1(&self, name: &str) -> f32 {
match self.action(name) {
Some(ActionValue::Axis1(axis)) => axis.value,
_ => 0.0,
}
}
#[must_use]
pub fn axis2(&self, name: &str) -> (f32, f32) {
match self.action(name) {
Some(ActionValue::Axis2(axis)) => (axis.x, axis.y),
_ => (0.0, 0.0),
}
}
#[must_use]
pub fn held_keys(&self) -> Vec<KeyCode> {
let mut held: Vec<(u64, KeyCode)> = self
.held_keys
.iter()
.map(|(&key, &serial)| (serial, key))
.collect();
held.sort_unstable();
held.into_iter().map(|(_, key)| key).collect()
}
#[must_use]
pub fn held_mouse_buttons(&self) -> Vec<PointerButton> {
let mut held: Vec<PointerButton> = self.held_buttons.iter().copied().collect();
held.sort_unstable();
held
}
#[must_use]
pub const fn pointer(&self) -> Option<(f32, f32)> {
self.pointer
}
fn button(&self, name: &str) -> Option<&ButtonAction> {
match self.action(name)? {
ActionValue::Button(button) => Some(button),
_ => None,
}
}
fn resolve_matching(&mut self, matches: impl Fn(&Binding) -> bool) {
for i in 0..self.slots.len() {
let slot = &self.slots[i];
if self.is_live(i) && slot.decl.bindings.iter().any(&matches) {
self.resolve_one(i);
}
}
}
fn resolve_one(&mut self, idx: usize) {
self.resolve_slot(idx);
self.drain_emits();
}
fn resolve_slot(&mut self, idx: usize) {
let control_sticks = &self.control_sticks;
let pads = &self.pads;
let mouse_delta = self.mouse_delta;
let scroll_delta = self.scroll_delta;
let pointer = self.pointer;
let elapsed = self.elapsed;
let slot = &mut self.slots[idx];
let view = View {
context: slot.context,
routes: &self.routes,
suppressed: &self.suppressed,
held_keys: &self.held_keys,
held_buttons: &self.held_buttons,
held_controls: &self.held_controls,
held_pad_buttons: self.held_pad_buttons,
};
let kind = slot.kind();
let bindings = &slot.decl.bindings;
match kind {
ActionKind::Button => {
let down = slot.emitted
|| bindings.iter().any(|b| match b {
Binding::Key(k) => view.key(*k),
Binding::Chord { modifier, key } => view.chord(*modifier, *key),
Binding::MouseButton(b) => view.button(*b),
Binding::ButtonChord { modifier, button } => {
view.button_chord(*modifier, *button)
}
Binding::Virtual(id) => view.control(id),
Binding::PadButton(button) => view.pad_button(*button),
Binding::PadChord { modifier, button } => {
view.pad_chord(*modifier, *button)
}
Binding::PadDpad => PadButton::DPAD.iter().any(|&b| view.pad_button(b)),
Binding::PadTrigger { trigger, threshold } => {
view.trigger(*trigger)
&& gamepad::pad_trigger(pads, *trigger) > *threshold
}
Binding::MouseMotion
| Binding::MouseScroll
| Binding::ScrollChord { .. }
| Binding::PointerPosition { .. }
| Binding::PadStick { .. } => false,
Binding::KeyAxis { negative, positive } => {
view.key(*negative) || view.key(*positive)
}
Binding::Wasd {
up,
down: w_down,
left,
right,
} => {
view.key(*up)
|| view.key(*w_down)
|| view.key(*left)
|| view.key(*right)
}
});
let was_active = slot.active;
slot.active = down;
let action = match &mut slot.value {
ActionValue::Button(a) => a,
_ => unreachable!(),
};
match (was_active, down) {
(false, true) => {
action.state = ButtonState::Pressed;
action.just_pressed = true;
slot.hold_start = Some(elapsed);
}
(true, true) => {
let duration = (elapsed - slot.hold_start.unwrap_or(elapsed)) as f32;
action.state = ButtonState::Held { duration };
}
(true, false) => {
action.state = ButtonState::Released;
action.just_released = true;
slot.hold_start = None;
}
(false, false) => {
action.state = ButtonState::Released;
}
}
}
ActionKind::Axis1 => {
let absolute = pointer.filter(|_| view.pointer()).and_then(|(px, py)| {
bindings.iter().find_map(|binding| match binding {
Binding::PointerPosition { axis } => Some(match axis {
PointerAxis::X => px,
PointerAxis::Y => py,
}),
_ => None,
})
});
let mut value: f32 = absolute.unwrap_or(0.0);
if absolute.is_none() {
for binding in bindings {
match binding {
Binding::MouseScroll if view.scroll() => {
value += scroll_delta.1;
}
Binding::ScrollChord { held } if view.scroll_chord(*held) => {
value += scroll_delta.1;
}
Binding::Key(k) if view.key(*k) => {
value += 1.0;
}
Binding::Chord { modifier, key } if view.chord(*modifier, *key) => {
value += 1.0;
}
Binding::PadButton(button) if view.pad_button(*button) => {
value += 1.0;
}
Binding::PadChord { modifier, button }
if view.pad_chord(*modifier, *button) =>
{
value += 1.0;
}
Binding::PadTrigger { trigger, threshold }
if view.trigger(*trigger) =>
{
value += gamepad::trigger_past(
gamepad::pad_trigger(pads, *trigger),
*threshold,
);
}
Binding::KeyAxis { negative, positive } => {
if view.key(*negative) {
value -= 1.0;
}
if view.key(*positive) {
value += 1.0;
}
}
_ => {}
}
}
}
value = value.clamp(-1.0, 1.0);
let action = match &mut slot.value {
ActionValue::Axis1(a) => a,
_ => unreachable!(),
};
action.value = value;
}
ActionKind::Axis2 => {
let mut dir_x: f32 = 0.0;
let mut dir_y: f32 = 0.0;
let mut x: f32 = 0.0;
let mut y: f32 = 0.0;
for binding in bindings {
match binding {
Binding::Wasd {
up,
down: w_down,
left,
right,
} => {
if view.key(*up) {
dir_y += 1.0;
}
if view.key(*w_down) {
dir_y -= 1.0;
}
if view.key(*left) {
dir_x -= 1.0;
}
if view.key(*right) {
dir_x += 1.0;
}
}
Binding::PadDpad => {
if view.pad_button(PadButton::DpadUp) {
dir_y += 1.0;
}
if view.pad_button(PadButton::DpadDown) {
dir_y -= 1.0;
}
if view.pad_button(PadButton::DpadLeft) {
dir_x -= 1.0;
}
if view.pad_button(PadButton::DpadRight) {
dir_x += 1.0;
}
}
Binding::Virtual(id) if view.stick(id) => {
if let Some((sx, sy)) = control_sticks.get(id.as_str()) {
dir_x += sx;
dir_y += sy;
}
}
Binding::PadStick { stick, deadzone } if view.pad_stick(*stick) => {
for pad in pads.values() {
let (sx, sy) =
gamepad::radial_deadzone(pad.stick(*stick), *deadzone);
dir_x += sx;
dir_y += sy;
}
}
Binding::MouseMotion if view.motion() => {
x += mouse_delta.0;
y += mouse_delta.1;
}
_ => {}
}
}
let len = (dir_x * dir_x + dir_y * dir_y).sqrt();
if len > 1.0 {
dir_x /= len;
dir_y /= len;
}
x += dir_x;
y += dir_y;
let action = match &mut slot.value {
ActionValue::Axis2(a) => a,
_ => unreachable!(),
};
action.x = x;
action.y = y;
}
}
if let Some(repeat) = &mut slot.repeat {
repeat.update(&slot.value, elapsed);
}
let before = slot.patterns.fired();
slot.patterns.update(&slot.value, elapsed);
slot.patterns.queue_emits(before, &mut self.pending_emits);
}
}
impl Default for ActionMap {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ActionMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActionMap")
.field(
"actions",
&self.slots.iter().map(|s| &s.decl.name).collect::<Vec<_>>(),
)
.field("held_keys", &self.held_keys.len())
.field("held_buttons", &self.held_buttons.len())
.field("held_controls", &self.held_controls)
.field("control_sticks", &self.control_sticks)
.field("pads", &self.pads)
.field("contexts", &self.active_contexts().collect::<Vec<_>>())
.field("devices", &self.devices)
.field("last_pad_kind", &self.last_pad_kind)
.field("pointer", &self.pointer)
.field("elapsed", &self.elapsed)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default)]
pub struct InputTickState {
pub actions: Vec<(String, ActionValue)>,
}
impl InputTickState {
#[must_use]
pub fn new() -> Self {
Self {
actions: Vec::new(),
}
}
pub fn capture(map: &ActionMap) -> Self {
let actions = map
.slots
.iter()
.map(|slot| (slot.decl.name.clone(), slot.value.clone()))
.collect();
Self { actions }
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&ActionValue> {
self.actions.iter().find(|(n, _)| n == name).map(|(_, v)| v)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn decl_button(name: &str, binding: Binding) -> ActionDecl {
ActionDecl {
name: name.to_owned(),
kind: ActionKind::Button,
bindings: vec![binding],
}
}
fn decl_axis2_wasd(
name: &str,
up: KeyCode,
down: KeyCode,
left: KeyCode,
right: KeyCode,
) -> ActionDecl {
ActionDecl {
name: name.to_owned(),
kind: ActionKind::Axis2,
bindings: vec![Binding::Wasd {
up,
down,
left,
right,
}],
}
}
fn decl_axis2_mouse(name: &str) -> ActionDecl {
ActionDecl {
name: name.to_owned(),
kind: ActionKind::Axis2,
bindings: vec![Binding::MouseMotion],
}
}
fn decl_axis1_scroll(name: &str) -> ActionDecl {
ActionDecl {
name: name.to_owned(),
kind: ActionKind::Axis1,
bindings: vec![Binding::MouseScroll],
}
}
const TICK: f32 = 1.0 / 60.0;
#[test]
fn a_button_is_held_while_it_is_down_and_not_after() {
let mut map = ActionMap::new();
map.declare(decl_button("thrust", Binding::Key(KeyCode::KeyW)));
assert!(!map.button_held("thrust"), "nothing is down yet");
map.begin_tick(TICK);
map.key_event(KeyCode::KeyW, true);
assert!(map.button_held("thrust"), "the tick the key arrives counts");
map.begin_tick(TICK);
assert!(
matches!(
map.action("thrust"),
Some(ActionValue::Button(ButtonAction {
state: ButtonState::Held { .. },
..
}))
),
"a second tick with the key still down is Held, which is the other \
state that must read as down"
);
assert!(map.button_held("thrust"));
map.key_event(KeyCode::KeyW, false);
assert!(!map.button_held("thrust"), "released is not down");
}
#[test]
fn the_edges_fire_once_each() {
let mut map = ActionMap::new();
map.declare(decl_button("flap", Binding::Key(KeyCode::Space)));
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
assert!(map.just_pressed("flap"));
assert!(!map.just_released("flap"));
map.begin_tick(TICK);
assert!(
!map.just_pressed("flap"),
"holding the key does not press it again — this is the whole \
reason a flap reads the edge and not the level"
);
assert!(map.button_held("flap"), "still down, though");
map.key_event(KeyCode::Space, false);
assert!(map.just_released("flap"));
map.begin_tick(TICK);
assert!(
!map.just_released("flap"),
"the release edge is one tick too"
);
}
#[test]
fn the_axis_accessors_read_the_value() {
let mut map = ActionMap::new();
map.declare(decl_axis2_wasd(
"move",
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::KeyA,
KeyCode::KeyD,
));
map.declare(decl_axis1_scroll("zoom"));
assert_eq!(map.axis2("move"), (0.0, 0.0));
assert_eq!(map.axis1("zoom"), 0.0);
map.begin_tick(TICK);
map.key_event(KeyCode::KeyD, true);
let (x, y) = map.axis2("move");
assert!((x - 1.0).abs() < 1e-6, "right alone is +1 on x, got {x}");
assert!(y.abs() < 1e-6, "and nothing on y, got {y}");
map.key_event(KeyCode::KeyW, true);
let (x, y) = map.axis2("move");
let length = x.hypot(y);
assert!(
(length - 1.0).abs() < 1e-6,
"a diagonal is normalised, or it is 41% faster than a cardinal: \
got length {length}"
);
map.mouse_scroll(0.0, 3.0);
assert_ne!(map.axis1("zoom"), 0.0, "the scroll reached the axis");
}
#[test]
fn the_wrong_kind_and_the_wrong_name_are_idle() {
let mut map = ActionMap::new();
map.declare(decl_button("fire", Binding::Key(KeyCode::Space)));
map.declare(decl_axis1_scroll("zoom"));
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
assert!(!map.button_held("zoom"), "an axis is not a held button");
assert!(!map.just_pressed("zoom"));
assert!(!map.just_released("zoom"));
assert_eq!(map.axis1("fire"), 0.0, "a button is not an axis");
assert_eq!(map.axis2("fire"), (0.0, 0.0));
assert!(!map.button_held("nonesuch"));
assert!(!map.just_pressed("nonesuch"));
assert_eq!(map.axis1("nonesuch"), 0.0);
assert_eq!(map.axis2("nonesuch"), (0.0, 0.0));
}
#[test]
fn button_action_default_is_released() {
let a = ButtonAction::default();
assert!(matches!(a.state, ButtonState::Released));
assert!(!a.just_pressed);
assert!(!a.just_released);
}
#[test]
fn reset_edges_clears_flags() {
let mut a = ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
just_released: true,
};
a.reset_edges();
assert!(!a.just_pressed);
assert!(!a.just_released);
assert!(matches!(a.state, ButtonState::Pressed));
}
#[test]
fn key_press_and_release() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
let v = map.action("jump").unwrap();
assert!(matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Released,
just_pressed: false,
just_released: false,
})
));
map.key_event(KeyCode::Space, true);
let v = map.action("jump").unwrap();
assert!(matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
just_released: false,
})
));
map.begin_tick(1.0 / 60.0);
let v = map.action("jump").unwrap();
assert!(matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Held { .. },
just_pressed: false,
just_released: false,
})
));
map.key_event(KeyCode::Space, false);
let v = map.action("jump").unwrap();
assert!(matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Released,
just_pressed: false,
just_released: true,
})
));
}
#[test]
fn a_key_still_down_after_a_tick_reports_held_with_how_long_it_has_been() {
let mut map = ActionMap::new();
map.declare(decl_button("fire", Binding::Key(KeyCode::KeyF)));
map.key_event(KeyCode::KeyF, true);
map.begin_tick(2.0);
let v = map.action("fire").unwrap();
if let ActionValue::Button(a) = v {
if let ButtonState::Held { duration } = a.state {
assert!(
(duration - 2.0).abs() < 0.001,
"expected ~2s, got {duration}"
);
} else {
panic!("expected Held, got {:?}", a.state);
}
} else {
panic!("expected Button, got {:?}", v);
}
}
#[test]
fn mouse_button_press_and_release() {
let mut map = ActionMap::new();
map.declare(decl_button(
"shoot",
Binding::MouseButton(PointerButton::Left),
));
map.mouse_button(PointerButton::Left, true);
let v = map.action("shoot").unwrap();
assert!(matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
just_released: false,
})
));
map.begin_tick(0.016);
map.mouse_button(PointerButton::Left, false);
let v = map.action("shoot").unwrap();
assert!(matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Released,
just_pressed: false,
just_released: true,
})
));
}
#[test]
fn a_wasd_composite_stays_a_unit_vector_from_idle_through_every_diagonal() {
let mut map = ActionMap::new();
map.declare(decl_axis2_wasd(
"move",
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::KeyA,
KeyCode::KeyD,
));
let v = map.action("move").unwrap();
assert_eq_axis2(v, 0.0, 0.0);
map.key_event(KeyCode::KeyW, true);
let v = map.action("move").unwrap();
assert_eq_axis2(v, 0.0, 1.0);
map.key_event(KeyCode::KeyD, true);
let v = map.action("move").unwrap();
let diag = std::f32::consts::FRAC_1_SQRT_2;
assert_eq_axis2(v, diag, diag);
map.key_event(KeyCode::KeyW, false);
map.key_event(KeyCode::KeyD, false);
map.key_event(KeyCode::KeyA, true);
map.key_event(KeyCode::KeyS, true);
let v = map.action("move").unwrap();
assert_eq_axis2(v, -diag, -diag);
map.key_event(KeyCode::KeyD, false);
map.key_event(KeyCode::KeyA, false);
map.key_event(KeyCode::KeyS, false);
let v = map.action("move").unwrap();
assert_eq_axis2(v, 0.0, 0.0);
}
#[test]
fn wasd_opposing_keys_cancel() {
let mut map = ActionMap::new();
map.declare(decl_axis2_wasd(
"move",
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::KeyA,
KeyCode::KeyD,
));
map.key_event(KeyCode::KeyW, true);
map.key_event(KeyCode::KeyS, true);
let v = map.action("move").unwrap();
assert_eq_axis2(v, 0.0, 0.0);
map.key_event(KeyCode::KeyA, true);
map.key_event(KeyCode::KeyD, true);
let v = map.action("move").unwrap();
assert_eq_axis2(v, 0.0, 0.0);
}
#[test]
fn mouse_motion_accumulates_within_a_tick_and_begin_tick_clears_it() {
let mut map = ActionMap::new();
map.declare(decl_axis2_mouse("look"));
map.mouse_motion(5.0, -3.0);
map.mouse_motion(2.0, 1.0);
let v = map.action("look").unwrap();
assert_eq_axis2(v, 7.0, -2.0);
map.begin_tick(0.016);
let v = map.action("look").unwrap();
assert_eq_axis2(v, 0.0, 0.0);
}
#[test]
fn scroll_sums_positive_and_negative_notches_and_resets_on_the_next_tick() {
let mut map = ActionMap::new();
map.declare(decl_axis1_scroll("zoom"));
map.mouse_scroll(0.0, 1.0);
let v = map.action("zoom").unwrap();
assert_eq_axis1(v, 1.0);
map.mouse_scroll(0.0, -0.5);
let v = map.action("zoom").unwrap();
assert_eq_axis1(v, 0.5);
map.begin_tick(0.016);
let v = map.action("zoom").unwrap();
assert_eq_axis1(v, 0.0);
}
#[test]
fn scroll_clamped_to_range() {
let mut map = ActionMap::new();
map.declare(decl_axis1_scroll("zoom"));
map.mouse_scroll(0.0, 5.0);
let v = map.action("zoom").unwrap();
assert_eq_axis1(v, 1.0);
map.begin_tick(0.016);
map.mouse_scroll(0.0, -5.0);
let v = map.action("zoom").unwrap();
assert_eq_axis1(v, -1.0);
}
fn decl_axis1_pointer(name: &str, axis: PointerAxis) -> ActionDecl {
ActionDecl {
name: name.to_owned(),
kind: ActionKind::Axis1,
bindings: vec![Binding::PointerPosition { axis }],
}
}
#[test]
fn a_pointer_position_lands_on_the_axis_it_is_bound_to() {
let mut map = ActionMap::new();
map.declare(decl_axis1_pointer("aim_x", PointerAxis::X));
map.declare(decl_axis1_pointer("aim_y", PointerAxis::Y));
assert_eq!(
map.axis1("aim_x"),
0.0,
"an axis nothing has pointed at yet is idle, not at the left edge",
);
map.begin_tick(TICK);
map.pointer_position(-0.75, 0.25);
assert_eq_axis1(map.action("aim_x").unwrap(), -0.75);
assert_eq_axis1(map.action("aim_y").unwrap(), 0.25);
}
#[test]
fn an_absolute_axis_holds_its_position_across_ticks() {
let mut map = ActionMap::new();
map.declare(decl_axis1_pointer("aim", PointerAxis::X));
map.declare(decl_axis1_scroll("zoom"));
map.begin_tick(TICK);
map.pointer_position(0.6, 0.0);
map.mouse_scroll(0.0, 1.0);
assert_eq_axis1(map.action("aim").unwrap(), 0.6);
assert_eq_axis1(map.action("zoom").unwrap(), 1.0);
map.begin_tick(TICK);
assert_eq_axis1(map.action("zoom").unwrap(), 0.0);
assert_eq_axis1(map.action("aim").unwrap(), 0.6);
}
#[test]
fn the_pointer_edge_is_one_tick_wide_and_only_for_a_new_position() {
let mut map = ActionMap::new();
map.declare(decl_axis1_pointer("aim", PointerAxis::X));
let moved = |map: &ActionMap| match map.action("aim") {
Some(ActionValue::Axis1(axis)) => axis.pointer_moved,
other => panic!("expected Axis1, got {other:?}"),
};
map.begin_tick(TICK);
assert!(!moved(&map), "nothing has pointed anywhere yet");
map.pointer_position(0.2, 0.0);
assert!(moved(&map), "the pointer moved on this tick");
map.begin_tick(TICK);
assert!(!moved(&map), "the edge is one tick wide");
assert_eq_axis1(map.action("aim").unwrap(), 0.2);
map.pointer_position(0.2, 0.0);
assert!(
!moved(&map),
"the same coordinate again is not the player asking for anything",
);
map.pointer_position(0.2, 0.9);
assert!(
moved(&map),
"the other coordinate changing is still the pointer moving",
);
}
#[test]
fn an_absolute_binding_replaces_the_relative_ones_on_the_same_axis() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "slide".to_owned(),
kind: ActionKind::Axis1,
bindings: vec![
Binding::KeyAxis {
negative: KeyCode::ArrowLeft,
positive: KeyCode::ArrowRight,
},
Binding::PointerPosition {
axis: PointerAxis::X,
},
],
});
map.begin_tick(TICK);
map.key_event(KeyCode::ArrowRight, true);
assert_eq_axis1(map.action("slide").unwrap(), 1.0);
map.pointer_position(-0.5, 0.0);
assert_eq_axis1(map.action("slide").unwrap(), -0.5);
}
#[test]
fn a_pointer_outside_the_surface_is_clamped_to_its_edge() {
let mut map = ActionMap::new();
map.declare(decl_axis1_pointer("aim", PointerAxis::X));
map.pointer_position(4.0, 0.0);
assert_eq_axis1(map.action("aim").unwrap(), 1.0);
map.pointer_position(-4.0, 0.0);
assert_eq_axis1(map.action("aim").unwrap(), -1.0);
}
#[test]
fn a_pointer_button_joins_the_keys_bound_to_the_same_action() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "flap".to_owned(),
kind: ActionKind::Button,
bindings: vec![
Binding::Key(KeyCode::Space),
Binding::MouseButton(PointerButton::Left),
],
});
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
assert!(map.just_pressed("flap"), "the key still flaps");
map.key_event(KeyCode::Space, false);
map.begin_tick(TICK);
map.mouse_button(PointerButton::Left, true);
assert!(map.just_pressed("flap"), "and so does the tap");
map.mouse_button(PointerButton::Left, false);
assert!(map.just_released("flap"));
}
#[test]
fn begin_tick_resets_just_pressed_and_just_released() {
let mut map = ActionMap::new();
map.declare(decl_button("a", Binding::Key(KeyCode::KeyA)));
map.key_event(KeyCode::KeyA, true);
assert!(matches!(
map.action("a").unwrap(),
ActionValue::Button(ButtonAction {
just_pressed: true,
..
})
));
map.begin_tick(0.016);
assert!(matches!(
map.action("a").unwrap(),
ActionValue::Button(ButtonAction {
just_pressed: false,
just_released: false,
..
})
));
map.key_event(KeyCode::KeyA, false);
assert!(matches!(
map.action("a").unwrap(),
ActionValue::Button(ButtonAction {
just_released: true,
..
})
));
map.begin_tick(0.016);
assert!(matches!(
map.action("a").unwrap(),
ActionValue::Button(ButtonAction {
just_released: false,
..
})
));
}
#[test]
fn disabled_action_returns_idle() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
map.key_event(KeyCode::Space, true);
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
..
})
));
map.set_enabled("jump", false);
let v = map.action("jump").unwrap();
assert!(matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Released,
just_pressed: false,
just_released: false,
})
));
map.set_enabled("jump", true);
map.key_event(KeyCode::Space, true); let v = map.action("jump").unwrap();
assert!(matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
..
})
));
}
#[test]
fn disabled_wasd_returns_zero() {
let mut map = ActionMap::new();
map.declare(decl_axis2_wasd(
"move",
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::KeyA,
KeyCode::KeyD,
));
map.key_event(KeyCode::KeyW, true);
assert_eq_axis2(map.action("move").unwrap(), 0.0, 1.0);
map.set_enabled("move", false);
assert_eq_axis2(map.action("move").unwrap(), 0.0, 0.0);
for tick in 0..3 {
map.begin_tick(1.0 / 60.0);
assert_eq_axis2(map.action("move").unwrap(), 0.0, 0.0);
assert!(tick < 3);
}
}
#[test]
fn a_disabled_button_does_not_re_press_itself_on_the_next_tick() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
map.key_event(KeyCode::Space, true);
map.set_enabled("jump", false);
for _ in 0..3 {
map.begin_tick(1.0 / 60.0);
let v = map.action("jump").unwrap();
assert!(
matches!(
v,
ActionValue::Button(ButtonAction {
state: ButtonState::Released,
just_pressed: false,
just_released: false,
})
),
"a disabled action fired anyway: {v:?}"
);
}
map.set_enabled("jump", true);
map.begin_tick(1.0 / 60.0);
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
..
})
));
}
#[test]
fn set_enabled_on_an_unknown_action_is_a_no_op() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
map.set_enabled("typo-in-the-profile", true);
map.set_enabled("typo-in-the-profile", false);
assert_eq!(map.is_enabled("typo-in-the-profile"), None);
assert_eq!(map.is_enabled("jump"), Some(true));
assert_eq!(map.action_names().count(), 1);
}
#[test]
fn try_declare_reports_a_duplicate_rather_than_aborting() {
let mut map = ActionMap::new();
assert!(
map.try_declare(decl_button("jump", Binding::Key(KeyCode::Space)))
.is_ok()
);
assert_eq!(
map.try_declare(decl_button("jump", Binding::Key(KeyCode::KeyJ))),
Err(ActionMapError::DuplicateName("jump".to_owned()))
);
assert_eq!(
map.bindings("jump"),
Some(&[Binding::Key(KeyCode::Space)][..])
);
}
#[test]
fn rebind_replaces_bindings_and_keeps_the_enabled_flag() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
map.set_enabled("jump", false);
map.rebind("jump", vec![Binding::Key(KeyCode::KeyJ)])
.expect("declared");
assert_eq!(
map.bindings("jump"),
Some(&[Binding::Key(KeyCode::KeyJ)][..])
);
assert_eq!(
map.is_enabled("jump"),
Some(false),
"the rebind must not silently re-enable the action"
);
map.set_enabled("jump", true);
map.key_event(KeyCode::KeyJ, true);
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
..
})
));
map.begin_tick(0.016);
map.key_event(KeyCode::KeyJ, false);
map.begin_tick(0.016);
map.key_event(KeyCode::Space, true);
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Released,
..
})
));
}
#[test]
fn rebind_an_unknown_action_errors() {
let mut map = ActionMap::new();
assert_eq!(
map.rebind("nope", vec![]),
Err(ActionMapError::UnknownAction("nope".to_owned()))
);
}
#[test]
fn rebind_resolves_against_the_current_held_keys() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
map.key_event(KeyCode::KeyJ, true);
map.rebind("jump", vec![Binding::Key(KeyCode::KeyJ)])
.expect("declared");
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
..
})
));
}
#[test]
fn key_axis_reaches_both_ends_of_the_range() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "throttle".into(),
kind: ActionKind::Axis1,
bindings: vec![Binding::KeyAxis {
negative: KeyCode::KeyS,
positive: KeyCode::KeyW,
}],
});
assert_eq_axis1(map.action("throttle").unwrap(), 0.0);
map.key_event(KeyCode::KeyW, true);
assert_eq_axis1(map.action("throttle").unwrap(), 1.0);
map.key_event(KeyCode::KeyS, true);
assert_eq_axis1(map.action("throttle").unwrap(), 0.0);
map.key_event(KeyCode::KeyW, false);
assert_eq_axis1(map.action("throttle").unwrap(), -1.0);
}
#[test]
fn key_axis_drives_a_button_from_either_key() {
let mut map = ActionMap::new();
map.declare(decl_button(
"any",
Binding::KeyAxis {
negative: KeyCode::KeyS,
positive: KeyCode::KeyW,
},
));
map.key_event(KeyCode::KeyS, true);
assert!(matches!(
map.action("any").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
..
})
));
}
#[test]
fn non_finite_deltas_are_dropped_at_the_ingest_boundary() {
let mut map = ActionMap::new();
map.declare(decl_axis2_mouse("look"));
map.declare(decl_axis1_scroll("zoom"));
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
map.mouse_motion(bad, 1.0);
map.mouse_motion(1.0, bad);
map.mouse_scroll(0.0, bad);
}
match map.action("look").unwrap() {
ActionValue::Axis2(a) => assert!(
a.x.is_finite() && a.y.is_finite(),
"look went non-finite: {a:?}"
),
other => panic!("expected Axis2, got {other:?}"),
}
match map.action("zoom").unwrap() {
ActionValue::Axis1(a) => {
assert!(a.value.is_finite(), "zoom went non-finite: {a:?}");
}
other => panic!("expected Axis1, got {other:?}"),
}
map.begin_tick(0.016);
map.mouse_motion(3.0, -4.0);
assert_eq_axis2(map.action("look").unwrap(), 3.0, -4.0);
}
#[test]
fn a_non_finite_pointer_position_is_dropped_and_the_last_one_survives() {
let mut map = ActionMap::new();
map.declare(decl_axis1_pointer("aim", PointerAxis::X));
map.pointer_position(0.4, 0.0);
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
map.pointer_position(bad, 0.0);
map.pointer_position(0.0, bad);
}
assert_eq_axis1(map.action("aim").unwrap(), 0.4);
}
#[test]
fn a_non_finite_dt_does_not_poison_hold_durations() {
let mut map = ActionMap::new();
map.declare(decl_button("fire", Binding::Key(KeyCode::KeyF)));
map.key_event(KeyCode::KeyF, true);
map.begin_tick(f32::NAN);
map.begin_tick(2.0);
let ActionValue::Button(a) = map.action("fire").unwrap() else {
panic!("expected Button");
};
let ButtonState::Held { duration } = a.state else {
panic!("expected Held, got {:?}", a.state);
};
assert!((duration - 2.0).abs() < 0.001, "got {duration}");
}
#[test]
fn a_negative_dt_does_not_report_a_negative_hold_duration() {
let mut map = ActionMap::new();
map.declare(decl_button("fire", Binding::Key(KeyCode::KeyF)));
map.key_event(KeyCode::KeyF, true);
map.begin_tick(-1.0);
let ActionValue::Button(a) = map.action("fire").unwrap() else {
panic!("expected Button");
};
let ButtonState::Held { duration } = a.state else {
panic!("expected Held, got {:?}", a.state);
};
assert!(
duration >= 0.0,
"a negative dt must not report a negative hold duration, got {duration}"
);
map.begin_tick(1.0);
let ActionValue::Button(a) = map.action("fire").unwrap() else {
panic!("expected Button");
};
let ButtonState::Held { duration } = a.state else {
panic!("expected Held, got {:?}", a.state);
};
assert!(
(duration - 1.0).abs() < 0.001,
"with the negative tick dropped the hold measures normally, got {duration}"
);
}
#[test]
fn action_mut_lets_caller_clear_edges() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
map.key_event(KeyCode::Space, true);
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
just_pressed: true,
..
})
));
if let Some(ActionValue::Button(a)) = map.action_mut("jump") {
a.just_pressed = false;
}
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
just_pressed: false,
..
})
));
}
#[test]
fn multiple_bindings_any_triggers() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "jump".into(),
kind: ActionKind::Button,
bindings: vec![
Binding::Key(KeyCode::Space),
Binding::MouseButton(PointerButton::Left),
],
});
map.key_event(KeyCode::Space, true);
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
..
})
));
map.begin_tick(0.016);
map.key_event(KeyCode::Space, false);
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Released,
..
})
));
map.begin_tick(0.016);
map.mouse_button(PointerButton::Left, true);
assert!(matches!(
map.action("jump").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
..
})
));
}
#[test]
fn multiple_bindings_stays_held_until_all_released() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "crouch".into(),
kind: ActionKind::Button,
bindings: vec![
Binding::Key(KeyCode::ControlLeft),
Binding::Key(KeyCode::KeyC),
],
});
map.key_event(KeyCode::ControlLeft, true);
assert!(matches!(
map.action("crouch").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
..
})
));
map.begin_tick(0.016);
map.key_event(KeyCode::KeyC, true);
assert!(matches!(
map.action("crouch").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Held { .. },
..
})
));
map.key_event(KeyCode::ControlLeft, false);
assert!(matches!(
map.action("crouch").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Held { .. },
..
})
));
map.key_event(KeyCode::KeyC, false);
assert!(matches!(
map.action("crouch").unwrap(),
ActionValue::Button(ButtonAction {
state: ButtonState::Released,
just_released: true,
..
})
));
}
#[test]
fn action_names_returns_in_order() {
let mut map = ActionMap::new();
map.declare(decl_button("a", Binding::Key(KeyCode::KeyA)));
map.declare(decl_button("b", Binding::Key(KeyCode::KeyB)));
map.declare(decl_button("c", Binding::Key(KeyCode::KeyC)));
let names: Vec<&str> = map.action_names().collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn tick_state_capture_and_get() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
map.declare(decl_axis2_wasd(
"move",
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::KeyA,
KeyCode::KeyD,
));
map.key_event(KeyCode::Space, true);
map.key_event(KeyCode::KeyW, true);
let tick = InputTickState::capture(&map);
assert_eq!(tick.actions.len(), 2);
let jump = tick.get("jump").unwrap();
assert!(matches!(
jump,
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
just_released: false,
})
));
let mv = tick.get("move").unwrap();
assert_eq_axis2(mv, 0.0, 1.0);
assert!(tick.get("nope").is_none());
}
#[test]
fn tick_state_new_is_empty() {
let tick = InputTickState::new();
assert!(tick.actions.is_empty());
assert!(tick.get("anything").is_none());
}
#[test]
fn two_actions_bound_to_one_key_both_report_pressed() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
map.declare(decl_button("fire", Binding::Key(KeyCode::Space)));
map.key_event(KeyCode::Space, true);
let jump = map.action("jump").unwrap();
let fire = map.action("fire").unwrap();
assert!(matches!(
jump,
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
..
})
));
assert!(matches!(
fire,
ActionValue::Button(ButtonAction {
state: ButtonState::Pressed,
just_pressed: true,
..
})
));
}
#[test]
fn unknown_action_returns_none() {
let mut map = ActionMap::new();
assert!(map.action("nope").is_none());
assert!(map.action_mut("nope").is_none());
}
#[test]
fn every_input_type_debug_prints_without_panicking() {
let mut map = ActionMap::new();
map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
let _ = format!("{map:?}");
let tick = InputTickState::capture(&map);
let _ = format!("{tick:?}");
let _ = format!("{:?}", ButtonAction::default());
let _ = format!("{:?}", ButtonState::Pressed);
let _ = format!("{:?}", ActionKind::Button);
let _ = format!(
"{:?}",
Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
}
);
}
#[test]
fn axis_defaults_are_zero() {
assert_eq!(
Axis1Action::default(),
Axis1Action {
value: 0.0,
pointer_moved: false,
}
);
assert_eq!(Axis2Action::default(), Axis2Action { x: 0.0, y: 0.0 });
}
#[test]
fn input_tick_state_default_is_empty() {
let tick = InputTickState::default();
assert!(tick.actions.is_empty());
}
#[test]
fn wasd_with_arrow_keys() {
let mut map = ActionMap::new();
map.declare(decl_axis2_wasd(
"move",
KeyCode::ArrowUp,
KeyCode::ArrowDown,
KeyCode::ArrowLeft,
KeyCode::ArrowRight,
));
map.key_event(KeyCode::ArrowRight, true);
map.key_event(KeyCode::ArrowUp, true);
let diag = std::f32::consts::FRAC_1_SQRT_2;
assert_eq_axis2(map.action("move").unwrap(), diag, diag);
}
#[test]
fn wasd_diagonal_is_not_faster_than_a_cardinal_direction() {
let mut map = ActionMap::new();
map.declare(decl_axis2_wasd(
"move",
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::KeyA,
KeyCode::KeyD,
));
map.key_event(KeyCode::KeyW, true);
let cardinal = axis2_len(map.action("move").unwrap());
assert!((cardinal - 1.0).abs() < 0.001, "got {cardinal}");
map.key_event(KeyCode::KeyD, true);
let diagonal = axis2_len(map.action("move").unwrap());
assert!(
(diagonal - 1.0).abs() < 0.001,
"diagonal magnitude {diagonal} should be 1.0"
);
}
#[test]
fn mouse_motion_is_not_normalized() {
let mut map = ActionMap::new();
map.declare(decl_axis2_mouse("look"));
map.mouse_motion(30.0, 40.0);
assert_eq_axis2(map.action("look").unwrap(), 30.0, 40.0);
}
#[test]
fn a_stick_and_a_keyboard_drive_one_action() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "move".to_owned(),
kind: ActionKind::Axis2,
bindings: vec![
Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
},
Binding::Virtual("stick_move".to_owned()),
],
});
assert_eq!(map.axis2("move"), (0.0, 0.0), "nothing is touching it yet");
map.virtual_stick("stick_move", -0.5, 0.25);
assert_eq_axis2(map.action("move").unwrap(), -0.5, 0.25);
map.begin_tick(TICK);
assert_eq_axis2(map.action("move").unwrap(), -0.5, 0.25);
map.key_event(KeyCode::KeyA, true);
let (x, y) = map.axis2("move");
let len = x.hypot(y);
assert!((len - 1.0).abs() < 0.001, "magnitude {len} left the disc");
assert!(
(y / x - 0.25 / -1.5).abs() < 0.001,
"the summed heading was not kept, got ({x}, {y})",
);
map.virtual_stick("stick_move", 0.0, 0.0);
map.key_event(KeyCode::KeyA, false);
assert_eq!(map.axis2("move"), (0.0, 0.0), "both let go");
}
#[test]
fn a_stick_past_its_radius_is_clamped_to_the_disc() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "move".to_owned(),
kind: ActionKind::Axis2,
bindings: vec![Binding::Virtual("stick_move".to_owned())],
});
map.virtual_stick("stick_move", 3.0, 4.0);
assert_eq_axis2(map.action("move").unwrap(), 0.6, 0.8);
let len = axis2_len(map.action("move").unwrap());
assert!((len - 1.0).abs() < 0.001, "got {len}");
}
#[test]
fn a_virtual_button_has_the_edges_a_key_has() {
let mut map = ActionMap::new();
map.declare(decl_button(
"pause",
Binding::Virtual("btn_pause".to_owned()),
));
map.begin_tick(TICK);
map.virtual_button("btn_pause", true);
assert!(map.just_pressed("pause"));
assert!(map.button_held("pause"));
map.begin_tick(TICK);
assert!(
!map.just_pressed("pause"),
"holding the control does not press it again",
);
assert!(map.button_held("pause"));
map.virtual_button("btn_pause", false);
assert!(map.just_released("pause"));
assert!(!map.button_held("pause"));
}
#[test]
fn a_control_drives_only_the_action_that_names_it() {
let mut map = ActionMap::new();
map.declare(decl_button("pause", Binding::Virtual("btn_pause".into())));
map.declare(ActionDecl {
name: "move".to_owned(),
kind: ActionKind::Axis2,
bindings: vec![Binding::Virtual("stick_move".to_owned())],
});
map.virtual_button("btn_other", true);
map.virtual_stick("stick_other", 1.0, 0.0);
assert!(!map.button_held("pause"), "another control's press");
assert_eq!(map.axis2("move"), (0.0, 0.0), "another control's stick");
map.virtual_button("btn_pause", true);
map.virtual_stick("stick_move", 1.0, 0.0);
assert!(map.button_held("pause"));
assert_eq!(map.axis2("move"), (1.0, 0.0));
}
#[test]
fn a_stick_on_a_one_dimensional_axis_reads_nothing() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "throttle".to_owned(),
kind: ActionKind::Axis1,
bindings: vec![Binding::Virtual("stick_move".to_owned())],
});
map.virtual_stick("stick_move", 1.0, 1.0);
assert_eq!(map.axis1("throttle"), 0.0);
}
#[test]
fn a_non_finite_deflection_is_dropped() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "move".to_owned(),
kind: ActionKind::Axis2,
bindings: vec![Binding::Virtual("stick_move".to_owned())],
});
map.virtual_stick("stick_move", 0.5, 0.5);
map.virtual_stick("stick_move", f32::NAN, 0.0);
assert_eq_axis2(map.action("move").unwrap(), 0.5, 0.5);
map.virtual_stick("stick_move", 0.0, f32::INFINITY);
assert_eq_axis2(map.action("move").unwrap(), 0.5, 0.5);
}
fn assert_eq_axis2(v: &ActionValue, x: f32, y: f32) {
match v {
ActionValue::Axis2(a) => {
assert!(
(a.x - x).abs() < 0.001 && (a.y - y).abs() < 0.001,
"expected Axis2({x}, {y}), got Axis2({}, {})",
a.x,
a.y
);
}
other => panic!("expected Axis2, got {other:?}"),
}
}
fn axis2_len(v: &ActionValue) -> f32 {
match v {
ActionValue::Axis2(a) => (a.x * a.x + a.y * a.y).sqrt(),
other => panic!("expected Axis2, got {other:?}"),
}
}
fn assert_eq_axis1(v: &ActionValue, val: f32) {
match v {
ActionValue::Axis1(a) => {
assert!(
(a.value - val).abs() < 0.001,
"expected Axis1({val}), got Axis1({})",
a.value
);
}
other => panic!("expected Axis1, got {other:?}"),
}
}
}