use sdl3_sys::everything::*;
use std::ffi::CStr;
use std::thread;
#[derive(Clone)]
pub struct InputAction {
pub setting_id: &'static str,
pub label: &'static str,
pub default_scancode: SDL_Scancode,
pub essential: bool,
pub mapping: Option<CapturedInput>,
}
#[derive(Clone)]
pub enum CapturedInput {
Keyboard {
scancode: SDL_Scancode,
name: String,
},
JoystickButton {
device_id: String,
button: u8,
name: String,
},
#[allow(dead_code)]
JoystickAxis {
device_id: String,
axis: u8,
name: String,
},
}
impl CapturedInput {
pub fn to_mapping_string(&self) -> String {
match self {
Self::Keyboard { scancode, .. } => {
format!("Key;{}", scancode.0)
}
Self::JoystickButton {
device_id, button, ..
} => {
format!("{device_id};{button}")
}
Self::JoystickAxis {
device_id, axis, ..
} => {
format!("{device_id};{axis}")
}
}
}
pub fn display_name(&self) -> &str {
match self {
Self::Keyboard { name, .. }
| Self::JoystickButton { name, .. }
| Self::JoystickAxis { name, .. } => name,
}
}
}
#[derive(Clone)]
pub enum JoystickEvent {
ButtonDown {
device_id: String,
button: u8,
name: String,
},
#[allow(dead_code)]
AxisMotion {
device_id: String,
axis: u8,
name: String,
},
AccelUpdate { x: f32, y: f32 },
PinscapeDetected { vpx_id: String },
DudesCabDetected { vpx_id: String },
PinOneDetected { vpx_id: String },
GamepadDetected { vpx_id: String, name: String },
}
unsafe fn vpx_device_id(joy: *mut sdl3_sys::everything::SDL_Joystick) -> String {
let serial_ptr = SDL_GetJoystickSerial(joy);
if !serial_ptr.is_null() {
let serial = CStr::from_ptr(serial_ptr).to_string_lossy();
if !serial.is_empty() {
return format!("SDLJoy_{serial}");
}
}
let guid = SDL_GetJoystickGUID(joy);
let mut buf = [0u8; 64];
SDL_GUIDToString(guid, buf.as_mut_ptr() as *mut _, buf.len() as i32);
let guid_str = CStr::from_ptr(buf.as_ptr() as *const _).to_string_lossy();
format!("SDLJoy_{guid_str}")
}
pub fn scancode_name(scancode: SDL_Scancode) -> String {
unsafe {
let name_ptr = SDL_GetScancodeName(scancode);
if !name_ptr.is_null() {
let s = CStr::from_ptr(name_ptr).to_string_lossy().into_owned();
if !s.is_empty() {
return s;
}
}
}
format!("Key {}", scancode.0)
}
pub fn egui_key_to_scancode(key: egui::Key) -> Option<SDL_Scancode> {
use egui::Key;
let sc = match key {
Key::A => SDL_SCANCODE_A,
Key::B => SDL_SCANCODE_B,
Key::C => SDL_SCANCODE_C,
Key::D => SDL_SCANCODE_D,
Key::E => SDL_SCANCODE_E,
Key::F => SDL_SCANCODE_F,
Key::G => SDL_SCANCODE_G,
Key::H => SDL_SCANCODE_H,
Key::I => SDL_SCANCODE_I,
Key::J => SDL_SCANCODE_J,
Key::K => SDL_SCANCODE_K,
Key::L => SDL_SCANCODE_L,
Key::M => SDL_SCANCODE_M,
Key::N => SDL_SCANCODE_N,
Key::O => SDL_SCANCODE_O,
Key::P => SDL_SCANCODE_P,
Key::Q => SDL_SCANCODE_Q,
Key::R => SDL_SCANCODE_R,
Key::S => SDL_SCANCODE_S,
Key::T => SDL_SCANCODE_T,
Key::U => SDL_SCANCODE_U,
Key::V => SDL_SCANCODE_V,
Key::W => SDL_SCANCODE_W,
Key::X => SDL_SCANCODE_X,
Key::Y => SDL_SCANCODE_Y,
Key::Z => SDL_SCANCODE_Z,
Key::Num0 => SDL_SCANCODE_0,
Key::Num1 => SDL_SCANCODE_1,
Key::Num2 => SDL_SCANCODE_2,
Key::Num3 => SDL_SCANCODE_3,
Key::Num4 => SDL_SCANCODE_4,
Key::Num5 => SDL_SCANCODE_5,
Key::Num6 => SDL_SCANCODE_6,
Key::Num7 => SDL_SCANCODE_7,
Key::Num8 => SDL_SCANCODE_8,
Key::Num9 => SDL_SCANCODE_9,
Key::Escape => SDL_SCANCODE_ESCAPE,
Key::Tab => SDL_SCANCODE_TAB,
Key::Space => SDL_SCANCODE_SPACE,
Key::Enter => SDL_SCANCODE_RETURN,
Key::Backspace => SDL_SCANCODE_BACKSPACE,
Key::Delete => SDL_SCANCODE_DELETE,
Key::Home => SDL_SCANCODE_HOME,
Key::End => SDL_SCANCODE_END,
Key::PageUp => SDL_SCANCODE_PAGEUP,
Key::PageDown => SDL_SCANCODE_PAGEDOWN,
Key::ArrowUp => SDL_SCANCODE_UP,
Key::ArrowDown => SDL_SCANCODE_DOWN,
Key::ArrowLeft => SDL_SCANCODE_LEFT,
Key::ArrowRight => SDL_SCANCODE_RIGHT,
Key::F1 => SDL_SCANCODE_F1,
Key::F2 => SDL_SCANCODE_F2,
Key::F3 => SDL_SCANCODE_F3,
Key::F4 => SDL_SCANCODE_F4,
Key::F5 => SDL_SCANCODE_F5,
Key::F6 => SDL_SCANCODE_F6,
Key::F7 => SDL_SCANCODE_F7,
Key::F8 => SDL_SCANCODE_F8,
Key::F9 => SDL_SCANCODE_F9,
Key::F10 => SDL_SCANCODE_F10,
Key::F11 => SDL_SCANCODE_F11,
Key::F12 => SDL_SCANCODE_F12,
Key::Minus => SDL_SCANCODE_MINUS,
Key::Comma => SDL_SCANCODE_COMMA,
Key::Period => SDL_SCANCODE_PERIOD,
_ => return None,
};
Some(sc)
}
pub fn egui_modifiers_to_scancode(modifiers: &egui::Modifiers) -> Option<SDL_Scancode> {
if modifiers.shift && !modifiers.ctrl && !modifiers.alt {
Some(SDL_SCANCODE_LSHIFT)
} else if modifiers.ctrl && !modifiers.shift && !modifiers.alt {
Some(SDL_SCANCODE_LCTRL)
} else if modifiers.alt && !modifiers.shift && !modifiers.ctrl {
Some(SDL_SCANCODE_LALT)
} else {
None
}
}
pub fn default_actions() -> Vec<InputAction> {
vec![
InputAction {
setting_id: "LeftFlipper",
label: "input_left_flipper",
default_scancode: SDL_SCANCODE_LSHIFT,
essential: true,
mapping: None,
},
InputAction {
setting_id: "RightFlipper",
label: "input_right_flipper",
default_scancode: SDL_SCANCODE_RSHIFT,
essential: true,
mapping: None,
},
InputAction {
setting_id: "LeftStagedFlipper",
label: "input_left_staged_flipper",
default_scancode: SDL_SCANCODE_LSHIFT,
essential: true,
mapping: None,
},
InputAction {
setting_id: "RightStagedFlipper",
label: "input_right_staged_flipper",
default_scancode: SDL_SCANCODE_RSHIFT,
essential: true,
mapping: None,
},
InputAction {
setting_id: "LeftMagna",
label: "input_left_magna",
default_scancode: SDL_SCANCODE_LCTRL,
essential: true,
mapping: None,
},
InputAction {
setting_id: "RightMagna",
label: "input_right_magna",
default_scancode: SDL_SCANCODE_RCTRL,
essential: true,
mapping: None,
},
InputAction {
setting_id: "Lockbar",
label: "input_lockbar",
default_scancode: SDL_SCANCODE_LALT,
essential: true,
mapping: None,
},
InputAction {
setting_id: "ExtraBall",
label: "input_extra_ball",
default_scancode: SDL_SCANCODE_B,
essential: true,
mapping: None,
},
InputAction {
setting_id: "LaunchBall",
label: "input_launch_ball",
default_scancode: SDL_SCANCODE_RETURN,
essential: true,
mapping: None,
},
InputAction {
setting_id: "Start",
label: "input_start",
default_scancode: SDL_SCANCODE_1,
essential: true,
mapping: None,
},
InputAction {
setting_id: "Credit1",
label: "input_add_credit",
default_scancode: SDL_SCANCODE_5,
essential: true,
mapping: None,
},
InputAction {
setting_id: "ExitGame",
label: "input_exit_game",
default_scancode: SDL_SCANCODE_ESCAPE,
essential: true,
mapping: None,
},
InputAction {
setting_id: "Credit2",
label: "input_credit_2",
default_scancode: SDL_SCANCODE_4,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Credit3",
label: "input_credit_3",
default_scancode: SDL_SCANCODE_3,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Credit4",
label: "input_credit_4",
default_scancode: SDL_SCANCODE_6,
essential: false,
mapping: None,
},
InputAction {
setting_id: "CoinDoor",
label: "input_coin_door",
default_scancode: SDL_SCANCODE_END,
essential: false,
mapping: None,
},
InputAction {
setting_id: "SlamTilt",
label: "input_slam_tilt",
default_scancode: SDL_SCANCODE_HOME,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Reset",
label: "input_reset",
default_scancode: SDL_SCANCODE_F3,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Service1",
label: "input_service1",
default_scancode: SDL_SCANCODE_7,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Service2",
label: "input_service2",
default_scancode: SDL_SCANCODE_8,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Service3",
label: "input_service3",
default_scancode: SDL_SCANCODE_9,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Service4",
label: "input_service4",
default_scancode: SDL_SCANCODE_0,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Service5",
label: "input_service5",
default_scancode: SDL_SCANCODE_6,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Service6",
label: "input_service6",
default_scancode: SDL_SCANCODE_PAGEUP,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Service7",
label: "input_service7",
default_scancode: SDL_SCANCODE_MINUS,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Service8",
label: "input_service8",
default_scancode: SDL_SCANCODE_UNKNOWN,
essential: false,
mapping: None,
},
InputAction {
setting_id: "LeftNudge",
label: "input_left_nudge",
default_scancode: SDL_SCANCODE_Z,
essential: false,
mapping: None,
},
InputAction {
setting_id: "RightNudge",
label: "input_right_nudge",
default_scancode: SDL_SCANCODE_SLASH,
essential: false,
mapping: None,
},
InputAction {
setting_id: "CenterNudge",
label: "input_center_nudge",
default_scancode: SDL_SCANCODE_SPACE,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Tilt",
label: "input_tilt",
default_scancode: SDL_SCANCODE_T,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Pause",
label: "input_pause",
default_scancode: SDL_SCANCODE_P,
essential: false,
mapping: None,
},
InputAction {
setting_id: "VolumeDown",
label: "input_volume_down",
default_scancode: SDL_SCANCODE_MINUS,
essential: false,
mapping: None,
},
InputAction {
setting_id: "VolumeUp",
label: "input_volume_up",
default_scancode: SDL_SCANCODE_EQUALS,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Custom1",
label: "input_custom1",
default_scancode: SDL_SCANCODE_UNKNOWN,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Custom2",
label: "input_custom2",
default_scancode: SDL_SCANCODE_UNKNOWN,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Custom3",
label: "input_custom3",
default_scancode: SDL_SCANCODE_UNKNOWN,
essential: false,
mapping: None,
},
InputAction {
setting_id: "Custom4",
label: "input_custom4",
default_scancode: SDL_SCANCODE_UNKNOWN,
essential: false,
mapping: None,
},
]
}
pub const PINSCAPE_PROFILES: &[&str] = &[
"KL25Z (KL Shield / Brain / Rig Master)",
"Pico (OpenPinballDevice)",
"DudesCab (Arnoz)",
"CSD PinOne",
];
pub fn pinscape_button_defaults(profile: usize) -> &'static [(&'static str, u8)] {
match profile {
0 => &[
("Start", 0),
("ExtraBall", 1),
("Credit1", 2),
("Credit2", 3),
("LaunchBall", 4),
("ExitGame", 5),
("LeftFlipper", 7),
("RightFlipper", 8),
("LeftMagna", 9),
("RightMagna", 10),
("LeftStagedFlipper", 7), ("RightStagedFlipper", 8), ("Lockbar", 11),
("Tilt", 12),
("CoinDoor", 13),
("Service1", 14),
("Service2", 15),
("Service3", 16),
("Service4", 17),
("VolumeDown", 19),
("VolumeUp", 20),
],
1 => &[
("Start", 0),
("ExitGame", 1),
("ExtraBall", 2),
("Credit1", 3),
("Credit2", 4),
("Credit3", 5),
("Credit4", 6),
("LaunchBall", 7),
("Lockbar", 8),
("LeftFlipper", 9),
("RightFlipper", 10),
("LeftStagedFlipper", 11),
("RightStagedFlipper", 12),
("LeftMagna", 13),
("RightMagna", 14),
("Tilt", 15),
("SlamTilt", 16),
("CoinDoor", 17),
("Service1", 18),
("Service2", 19),
("Service3", 20),
("Service4", 21),
("LeftNudge", 22),
("CenterNudge", 23),
("RightNudge", 24),
("VolumeUp", 25),
("VolumeDown", 26),
],
2 => &[
("Start", 0), ("ExtraBall", 1), ("Credit1", 2), ("Credit2", 3), ("LaunchBall", 4), ("ExitGame", 5), ("LeftFlipper", 7), ("RightFlipper", 8), ("LeftMagna", 9), ("RightMagna", 10), ("LeftStagedFlipper", 7), ("RightStagedFlipper", 8), ("Tilt", 11), ("Lockbar", 12), ("CoinDoor", 13), ("Service1", 14), ("Service2", 15), ("Service3", 16), ("Service4", 17), ("VolumeDown", 18), ("VolumeUp", 19), ],
_ => &[
("RightFlipper", 0), ("RightMagna", 1), ("LeftFlipper", 2), ("LeftMagna", 3), ("ExtraBall", 4), ("Start", 5), ("Credit1", 6), ("ExitGame", 7), ("Lockbar", 8), ("VolumeUp", 15), ("VolumeDown", 16), ("CoinDoor", 17), ("Service1", 18), ("Service2", 19), ("Service3", 20), ("Service4", 21), ("Tilt", 22), ("LaunchBall", 23), ],
}
}
pub fn find_conflicts(actions: &[InputAction]) -> Vec<(usize, usize)> {
let mut conflicts = Vec::new();
for i in 0..actions.len() {
for j in (i + 1)..actions.len() {
let a = effective_scancode(&actions[i]);
let b = effective_scancode(&actions[j]);
if a != SDL_SCANCODE_UNKNOWN && a == b {
conflicts.push((i, j));
}
}
}
conflicts
}
pub(crate) fn effective_scancode(action: &InputAction) -> SDL_Scancode {
match &action.mapping {
Some(CapturedInput::Keyboard { scancode, .. }) => *scancode,
None => action.default_scancode,
_ => SDL_SCANCODE_UNKNOWN,
}
}
struct OpenedJoystick {
handle: *mut sdl3_sys::everything::SDL_Joystick,
vpx_id: String,
num_buttons: i32,
num_axes: i32,
prev_buttons: Vec<bool>,
button_names: Vec<String>,
axis_names: Vec<String>,
}
pub fn spawn_joystick_thread() -> crossbeam_channel::Receiver<JoystickEvent> {
let (evt_tx, evt_rx) = crossbeam_channel::unbounded::<JoystickEvent>();
thread::spawn(move || {
unsafe {
if !SDL_InitSubSystem(SDL_INIT_JOYSTICK) {
log::error!(
"Joystick thread: SDL_InitSubSystem failed: {:?}",
CStr::from_ptr(SDL_GetError())
);
return;
}
let mut joy_count: i32 = 0;
let joy_ids = SDL_GetJoysticks(&mut joy_count);
let mut joysticks: Vec<OpenedJoystick> = Vec::with_capacity(joy_count as usize);
if !joy_ids.is_null() {
for i in 0..joy_count as usize {
let jid = *joy_ids.add(i);
let joy = SDL_OpenJoystick(jid);
if !joy.is_null() {
let name_ptr = SDL_GetJoystickName(joy);
let name = if !name_ptr.is_null() {
CStr::from_ptr(name_ptr).to_string_lossy().into_owned()
} else {
format!("Joystick {}", jid.0)
};
let dev_id = vpx_device_id(joy);
let num_buttons = SDL_GetNumJoystickButtons(joy);
let num_axes = SDL_GetNumJoystickAxes(joy);
log::info!(
"Opened joystick: {} (vpx_id={}, buttons={}, axes={})",
name,
dev_id,
num_buttons,
num_axes
);
if name.contains("Pinscape") || dev_id.contains("PSC") {
log::info!("Pinscape controller detected: {}", dev_id);
let _ = evt_tx.send(JoystickEvent::PinscapeDetected {
vpx_id: dev_id.clone(),
});
} else if name.contains("DudesCab") {
log::info!("DudesCab controller detected: {}", dev_id);
let _ = evt_tx.send(JoystickEvent::DudesCabDetected {
vpx_id: dev_id.clone(),
});
} else if name.contains("PinOne") {
log::info!("CSD PinOne controller detected: {}", dev_id);
let _ = evt_tx.send(JoystickEvent::PinOneDetected {
vpx_id: dev_id.clone(),
});
} else if SDL_IsGamepad(jid) {
log::info!("Gamepad detected: {} ({})", name, dev_id);
let _ = evt_tx.send(JoystickEvent::GamepadDetected {
vpx_id: dev_id.clone(),
name: name.clone(),
});
}
let button_names: Vec<String> = (0..num_buttons)
.map(|b| format!("{} Button {}", dev_id, b))
.collect();
let axis_names: Vec<String> = (0..num_axes)
.map(|a| format!("{} Axis {}", dev_id, a))
.collect();
joysticks.push(OpenedJoystick {
handle: joy,
vpx_id: dev_id,
num_buttons,
num_axes,
prev_buttons: vec![false; num_buttons as usize],
button_names,
axis_names,
});
}
}
SDL_free(joy_ids as *mut _);
}
log::info!(
"Joystick thread started, {} joystick(s) found",
joysticks.len()
);
loop {
SDL_UpdateJoysticks();
for js in &mut joysticks {
for b in 0..js.num_buttons {
let pressed = SDL_GetJoystickButton(js.handle, b);
let idx = b as usize;
let was_pressed = js.prev_buttons[idx];
if pressed && !was_pressed {
let _ = evt_tx.send(JoystickEvent::ButtonDown {
device_id: js.vpx_id.clone(),
button: b as u8,
name: js.button_names[idx].clone(),
});
}
js.prev_buttons[idx] = pressed;
}
for a in 0..js.num_axes {
let value = SDL_GetJoystickAxis(js.handle, a);
if value.abs() > 16000 {
let _ = evt_tx.send(JoystickEvent::AxisMotion {
device_id: js.vpx_id.clone(),
axis: a as u8,
name: js.axis_names[a as usize].clone(),
});
}
}
if js.num_axes >= 2 {
let ax = SDL_GetJoystickAxis(js.handle, 0) as f32 / 32767.0;
let ay = SDL_GetJoystickAxis(js.handle, 1) as f32 / 32767.0;
let _ = evt_tx.send(JoystickEvent::AccelUpdate { x: ax, y: ay });
}
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
});
evt_rx
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keyboard_mapping_string() {
let input = CapturedInput::Keyboard {
scancode: SDL_SCANCODE_LSHIFT,
name: "Left Shift".to_string(),
};
assert_eq!(
input.to_mapping_string(),
format!("Key;{}", SDL_SCANCODE_LSHIFT.0)
);
}
#[test]
fn joystick_button_mapping_string() {
let input = CapturedInput::JoystickButton {
device_id: "SDLJoy_PSC004".to_string(),
button: 7,
name: "Button 7".to_string(),
};
assert_eq!(input.to_mapping_string(), "SDLJoy_PSC004;7");
}
#[test]
fn joystick_axis_mapping_string() {
let input = CapturedInput::JoystickAxis {
device_id: "SDLJoy_ABC".to_string(),
axis: 2,
name: "Axis 2".to_string(),
};
assert_eq!(input.to_mapping_string(), "SDLJoy_ABC;2");
}
#[test]
fn display_name_keyboard() {
let input = CapturedInput::Keyboard {
scancode: SDL_SCANCODE_A,
name: "A".to_string(),
};
assert_eq!(input.display_name(), "A");
}
#[test]
fn display_name_joystick() {
let input = CapturedInput::JoystickButton {
device_id: "dev".to_string(),
button: 0,
name: "Start Button".to_string(),
};
assert_eq!(input.display_name(), "Start Button");
}
#[test]
fn default_actions_has_essential_and_advanced() {
let actions = default_actions();
assert!(!actions.is_empty());
let essential_count = actions.iter().filter(|a| a.essential).count();
let advanced_count = actions.iter().filter(|a| !a.essential).count();
assert!(
essential_count >= 10,
"expected >=10 essential actions, got {essential_count}"
);
assert!(
advanced_count >= 10,
"expected >=10 advanced actions, got {advanced_count}"
);
}
#[test]
fn default_actions_all_have_setting_id() {
for action in default_actions() {
assert!(!action.setting_id.is_empty(), "action has empty setting_id");
}
}
#[test]
fn default_actions_none_have_mapping() {
for action in default_actions() {
assert!(
action.mapping.is_none(),
"{} should have no mapping by default",
action.setting_id
);
}
}
#[test]
fn effective_scancode_default() {
let action = InputAction {
setting_id: "Test",
label: "test",
default_scancode: SDL_SCANCODE_A,
essential: true,
mapping: None,
};
assert!(effective_scancode(&action) == SDL_SCANCODE_A);
}
#[test]
fn effective_scancode_with_keyboard_mapping() {
let action = InputAction {
setting_id: "Test",
label: "test",
default_scancode: SDL_SCANCODE_A,
essential: true,
mapping: Some(CapturedInput::Keyboard {
scancode: SDL_SCANCODE_B,
name: "B".to_string(),
}),
};
assert!(effective_scancode(&action) == SDL_SCANCODE_B);
}
#[test]
fn effective_scancode_with_joystick_is_unknown() {
let action = InputAction {
setting_id: "Test",
label: "test",
default_scancode: SDL_SCANCODE_A,
essential: true,
mapping: Some(CapturedInput::JoystickButton {
device_id: "dev".to_string(),
button: 0,
name: "B0".to_string(),
}),
};
assert!(effective_scancode(&action) == SDL_SCANCODE_UNKNOWN);
}
#[test]
fn find_conflicts_no_conflicts() {
let actions = vec![
InputAction {
setting_id: "A",
label: "a",
default_scancode: SDL_SCANCODE_A,
essential: true,
mapping: None,
},
InputAction {
setting_id: "B",
label: "b",
default_scancode: SDL_SCANCODE_B,
essential: true,
mapping: None,
},
];
assert!(find_conflicts(&actions).is_empty());
}
#[test]
fn find_conflicts_detects_default_conflict() {
let actions = vec![
InputAction {
setting_id: "A",
label: "a",
default_scancode: SDL_SCANCODE_LSHIFT,
essential: true,
mapping: None,
},
InputAction {
setting_id: "B",
label: "b",
default_scancode: SDL_SCANCODE_LSHIFT,
essential: true,
mapping: None,
},
];
let conflicts = find_conflicts(&actions);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0], (0, 1));
}
#[test]
fn find_conflicts_unknown_scancode_not_conflicting() {
let actions = vec![
InputAction {
setting_id: "A",
label: "a",
default_scancode: SDL_SCANCODE_UNKNOWN,
essential: false,
mapping: None,
},
InputAction {
setting_id: "B",
label: "b",
default_scancode: SDL_SCANCODE_UNKNOWN,
essential: false,
mapping: None,
},
];
assert!(find_conflicts(&actions).is_empty());
}
#[test]
fn find_conflicts_with_mapped_override() {
let actions = vec![
InputAction {
setting_id: "A",
label: "a",
default_scancode: SDL_SCANCODE_A,
essential: true,
mapping: Some(CapturedInput::Keyboard {
scancode: SDL_SCANCODE_Z,
name: "Z".to_string(),
}),
},
InputAction {
setting_id: "B",
label: "b",
default_scancode: SDL_SCANCODE_Z,
essential: true,
mapping: None,
},
];
let conflicts = find_conflicts(&actions);
assert_eq!(conflicts.len(), 1);
}
#[test]
fn find_conflicts_joystick_mapping_no_conflict_with_keyboard() {
let actions = vec![
InputAction {
setting_id: "A",
label: "a",
default_scancode: SDL_SCANCODE_A,
essential: true,
mapping: Some(CapturedInput::JoystickButton {
device_id: "dev".to_string(),
button: 0,
name: "B0".to_string(),
}),
},
InputAction {
setting_id: "B",
label: "b",
default_scancode: SDL_SCANCODE_A,
essential: true,
mapping: None,
},
];
assert!(find_conflicts(&actions).is_empty());
}
#[test]
fn egui_key_letters() {
assert!(egui_key_to_scancode(egui::Key::A) == Some(SDL_SCANCODE_A));
assert!(egui_key_to_scancode(egui::Key::Z) == Some(SDL_SCANCODE_Z));
}
#[test]
fn egui_key_numbers() {
assert!(egui_key_to_scancode(egui::Key::Num0) == Some(SDL_SCANCODE_0));
assert!(egui_key_to_scancode(egui::Key::Num9) == Some(SDL_SCANCODE_9));
}
#[test]
fn egui_key_special() {
assert!(egui_key_to_scancode(egui::Key::Escape) == Some(SDL_SCANCODE_ESCAPE));
assert!(egui_key_to_scancode(egui::Key::Enter) == Some(SDL_SCANCODE_RETURN));
assert!(egui_key_to_scancode(egui::Key::Space) == Some(SDL_SCANCODE_SPACE));
}
#[test]
fn egui_key_function_keys() {
assert!(egui_key_to_scancode(egui::Key::F1) == Some(SDL_SCANCODE_F1));
assert!(egui_key_to_scancode(egui::Key::F12) == Some(SDL_SCANCODE_F12));
}
#[test]
fn egui_key_unmapped_returns_none() {
assert!(egui_key_to_scancode(egui::Key::Insert).is_none());
}
#[test]
fn modifiers_shift_only() {
let m = egui::Modifiers {
shift: true,
ctrl: false,
alt: false,
..Default::default()
};
assert!(egui_modifiers_to_scancode(&m) == Some(SDL_SCANCODE_LSHIFT));
}
#[test]
fn modifiers_ctrl_only() {
let m = egui::Modifiers {
ctrl: true,
shift: false,
alt: false,
..Default::default()
};
assert!(egui_modifiers_to_scancode(&m) == Some(SDL_SCANCODE_LCTRL));
}
#[test]
fn modifiers_alt_only() {
let m = egui::Modifiers {
alt: true,
shift: false,
ctrl: false,
..Default::default()
};
assert!(egui_modifiers_to_scancode(&m) == Some(SDL_SCANCODE_LALT));
}
#[test]
fn modifiers_multiple_returns_none() {
let m = egui::Modifiers {
shift: true,
ctrl: true,
alt: false,
..Default::default()
};
assert!(egui_modifiers_to_scancode(&m).is_none());
}
#[test]
fn modifiers_none_returns_none() {
let m = egui::Modifiers::default();
assert!(egui_modifiers_to_scancode(&m).is_none());
}
#[test]
fn default_actions_known_conflicts() {
let actions = default_actions();
let conflicts = find_conflicts(&actions);
assert!(
!conflicts.is_empty(),
"expected known default conflicts (LSHIFT, RSHIFT, MINUS, 6)"
);
let find_pair = |id_a: &str, id_b: &str| -> bool {
conflicts.iter().any(|&(i, j)| {
(actions[i].setting_id == id_a && actions[j].setting_id == id_b)
|| (actions[i].setting_id == id_b && actions[j].setting_id == id_a)
})
};
assert!(
find_pair("LeftFlipper", "LeftStagedFlipper"),
"LSHIFT conflict missing"
);
assert!(
find_pair("RightFlipper", "RightStagedFlipper"),
"RSHIFT conflict missing"
);
assert!(
find_pair("VolumeDown", "Service7"),
"MINUS conflict missing"
);
assert!(find_pair("Credit4", "Service5"), "6 key conflict missing");
}
}