1use crate::app::GenericButton; 2use crate::app::input::InputMappingSet; 3use egui::{RichText, Window}; 4use jgenesis_native_config::input::{ 5 AxisDirection, GamepadAction, GenericInput, HatDirection, InputAppConfig, KeyboardInput, 6}; 7use jgenesis_native_driver::input::Joysticks; 8use sdl3::event::{Event, WindowEvent}; 9use sdl3::joystick::{HatState, Joystick}; 10use sdl3::keyboard::{Keycode, Scancode}; 11use std::collections::{HashMap, HashSet}; 12use std::mem; 13 14struct VecSet(Vec<GenericInput>); 15 16impl VecSet { 17 fn new() -> Self { 18 Self(Vec::new()) 19 } 20 21 fn insert(&mut self, input: GenericInput) { 22 if !self.0.contains(&input) { 23 self.0.push(input); 24 } 25 } 26 27 fn len(&self) -> usize { 28 self.0.len() 29 } 30} 31 32#[derive(Debug, Clone, Copy, PartialEq, Eq)] 33enum CollectionDone { 34 No, 35 Yes, 36} 37 38struct CollectedInputs { 39 inputs: VecSet, 40 gamepad_starting_states: HashSet<GenericInput>, 41 initial_axis_directions: HashMap<(u32, u8), AxisDirection>, 42} 43 44impl CollectedInputs { 45 fn new(joysticks: &Joysticks, axis_deadzone: i16) -> Self { 46 let gamepad_starting_states = joysticks 47 .all_devices() 48 .flat_map(|(device_id, (joystick, _))| { 49 log::debug!("Added device {device_id} '{}'", joystick.name()); 50 joystick_starting_state(device_id, joystick, axis_deadzone) 51 }) 52 .collect(); 53 54 let initial_axis_directions = joysticks 55 .all_devices() 56 .flat_map(|(gamepad_idx, (_, initial_axis_directions))| { 57 initial_axis_directions 58 .iter() 59 .map(move |&(axis_idx, direction)| ((gamepad_idx, axis_idx), direction)) 60 }) 61 .collect(); 62 63 log::debug!("Gamepad starting states: {gamepad_starting_states:?}"); 64 65 Self { inputs: VecSet::new(), gamepad_starting_states, initial_axis_directions } 66 } 67 68 fn add_device( 69 &mut self, 70 joysticks: &Joysticks, 71 device_id: u32, 72 joystick: &Joystick, 73 axis_deadzone: i16, 74 ) { 75 log::debug!("Added device {device_id} '{}'", joystick.name()); 76 77 self.gamepad_starting_states.extend(joystick_starting_state( 78 device_id, 79 joystick, 80 axis_deadzone, 81 )); 82 83 if let Some(initial_axis_directions) = joysticks.initial_axis_directions(device_id) { 84 self.initial_axis_directions.extend( 85 initial_axis_directions 86 .map(|(axis_idx, direction)| ((device_id, axis_idx), direction)), 87 ); 88 } 89 90 log::debug!("Gamepad starting states: {:?}", self.gamepad_starting_states); 91 } 92 93 fn remove_device(&mut self, device_id: u32) { 94 self.gamepad_starting_states.retain(|input| !matches!(*input, GenericInput::Gamepad { gamepad_idx, .. } if gamepad_idx == device_id)); 95 96 self.initial_axis_directions.retain(|&(gamepad_idx, _), _| gamepad_idx != device_id); 97 } 98 99 fn contains(&self, input: GenericInput) -> bool { 100 self.inputs.0.contains(&input) 101 } 102 103 fn consume(self) -> Vec<GenericInput> { 104 // Don't allow axis inputs in combination with other inputs. 105 // This is to work around some controllers sending analog triggers as both an axis and a 106 // button (e.g. 8BitDo Pro 2), as well as to prevent accidentally inputting two axis 107 // directions simultaneously 108 if let Some(&axis_input) = self.inputs.0.iter().find(|input| { 109 matches!(input, GenericInput::Gamepad { action: GamepadAction::Axis(..), .. }) 110 }) { 111 return vec![axis_input]; 112 } 113 114 self.inputs.0 115 } 116 117 #[must_use] 118 fn insert(&mut self, input: GenericInput) -> CollectionDone { 119 match input { 120 GenericInput::Gamepad { 121 gamepad_idx, 122 action: GamepadAction::Axis(axis_idx, direction), 123 } => { 124 let opposite_input = GenericInput::Gamepad { 125 gamepad_idx, 126 action: GamepadAction::Axis(axis_idx, direction.inverse()), 127 }; 128 if self.contains(opposite_input) { 129 return CollectionDone::Yes; 130 } 131 132 self.gamepad_starting_states.remove(&opposite_input); 133 134 if self.gamepad_starting_states.contains(&input) 135 || self 136 .initial_axis_directions 137 .get(&(gamepad_idx, axis_idx)) 138 .is_some_and(|&initial_direction| direction == initial_direction) 139 { 140 return CollectionDone::No; 141 } 142 } 143 _ => { 144 if self.gamepad_starting_states.remove(&input) { 145 return CollectionDone::No; 146 } 147 } 148 } 149 150 self.inputs.insert(input); 151 if self.inputs.len() == jgenesis_native_driver::input::MAX_MAPPING_LEN { 152 CollectionDone::Yes 153 } else { 154 CollectionDone::No 155 } 156 } 157 158 #[must_use] 159 fn axis_zero(&mut self, gamepad_idx: u32, axis_idx: u8) -> CollectionDone { 160 for direction in [AxisDirection::Positive, AxisDirection::Negative] { 161 let input = GenericInput::Gamepad { 162 gamepad_idx, 163 action: GamepadAction::Axis(axis_idx, direction), 164 }; 165 if self.contains(input) { 166 return CollectionDone::Yes; 167 } 168 169 self.gamepad_starting_states.remove(&input); 170 } 171 172 CollectionDone::No 173 } 174} 175 176pub struct InputCollectionState { 177 buttons: Vec<GenericButton>, 178 mapping: InputMappingSet, 179 turbo: bool, 180 inputs: CollectedInputs, 181 aborted: bool, 182 mouse_over_window: bool, 183} 184 185impl InputCollectionState { 186 // Use a fairly high deadzone for detecting axis directions to make it harder to accidentally 187 // input the wrong direction 188 const AXIS_DEADZONE: i16 = 27000; 189 190 pub fn new( 191 joysticks: &Joysticks, 192 buttons: Vec<GenericButton>, 193 mapping: InputMappingSet, 194 turbo: bool, 195 ) -> Self { 196 let collected_inputs = CollectedInputs::new(joysticks, Self::AXIS_DEADZONE); 197 198 Self { 199 buttons, 200 mapping, 201 turbo, 202 inputs: collected_inputs, 203 aborted: false, 204 mouse_over_window: false, 205 } 206 } 207 208 pub fn done(&self) -> bool { 209 self.aborted || self.buttons.is_empty() 210 } 211 212 pub fn show_window(&mut self, ctx: &egui::Context, joysticks: &Joysticks) { 213 const TITLE: &str = "Input Configuration"; 214 215 let mut open = !self.done(); 216 if !open { 217 return; 218 } 219 220 // Prevent input configuration window from ever going below another window 221 ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, egui::Id::new(TITLE))); 222 223 Window::new(TITLE).open(&mut open).resizable(false).collapsible(false).show(ctx, |ui| { 224 self.mouse_over_window = ui.rect_contains_pointer(ui.max_rect()); 225 render_input_window(joysticks, self.buttons[0], ui); 226 }); 227 228 self.aborted = !open; 229 } 230 231 pub fn handle_sdl_event( 232 &mut self, 233 event: &Event, 234 ctx: &egui::Context, 235 gui_window_id: u32, 236 joysticks: &mut Joysticks, 237 input_config: &mut InputAppConfig, 238 ) { 239 if self.done() { 240 return; 241 } 242 243 if matches!( 244 event, 245 Event::JoyDeviceAdded { .. } 246 | Event::JoyDeviceRemoved { .. } 247 | Event::JoyButtonUp { .. } 248 | Event::JoyButtonDown { .. } 249 | Event::JoyAxisMotion { .. } 250 | Event::JoyHatMotion { .. } 251 ) { 252 ctx.request_repaint(); 253 } 254 255 if let Some(input) = self.maybe_collect_input(event, gui_window_id, joysticks) { 256 let button = self.buttons[0]; 257 258 log::info!("Received input {input:?} for button {button:?}"); 259 260 if !input.is_empty() 261 && let Some(value) = 262 button.access_value_maybe_turbo(self.mapping, input_config, self.turbo) 263 { 264 *value = Some(input); 265 } 266 267 self.buttons.remove(0); 268 ctx.request_repaint(); 269 } 270 } 271 272 fn maybe_collect_input( 273 &mut self, 274 event: &Event, 275 gui_window_id: u32, 276 joysticks: &mut Joysticks, 277 ) -> Option<Vec<GenericInput>> { 278 if self.aborted || self.buttons.is_empty() { 279 return None; 280 } 281 282 match *event { 283 Event::Quit { .. } => { 284 self.aborted = true; 285 } 286 Event::Window { window_id, win_event: WindowEvent::CloseRequested, .. } 287 if window_id == gui_window_id => 288 { 289 self.aborted = true; 290 } 291 Event::KeyDown { keycode, scancode, window_id, .. } 292 if window_id == gui_window_id 293 && let Some(key) = keyboard_input_for(keycode, scancode) 294 && self.inputs.insert(GenericInput::Keyboard(key)) == CollectionDone::Yes => 295 { 296 return self.consume_collected_inputs(joysticks); 297 } 298 Event::KeyUp { keycode, scancode, window_id, .. } 299 if window_id == gui_window_id 300 && let Some(key) = keyboard_input_for(keycode, scancode) 301 && self.inputs.contains(GenericInput::Keyboard(key)) => 302 { 303 return self.consume_collected_inputs(joysticks); 304 } 305 Event::JoyDeviceAdded { which: joystick_id, .. } => { 306 if let Err(err) = joysticks.handle_device_added(joystick_id) { 307 log::error!("Error adding joystick with joystick id {joystick_id}: {err}"); 308 } 309 310 if let Some(gamepad_idx) = joysticks.map_to_device_id(joystick_id) 311 && let Some(joystick) = joysticks.device(gamepad_idx) 312 { 313 self.inputs.add_device(joysticks, gamepad_idx, joystick, Self::AXIS_DEADZONE); 314 } 315 } 316 Event::JoyDeviceRemoved { which: joystick_id, .. } => { 317 if let Some(gamepad_idx) = joysticks.map_to_device_id(joystick_id) { 318 self.inputs.remove_device(gamepad_idx); 319 } 320 321 if let Err(err) = joysticks.handle_device_removed(joystick_id) { 322 log::error!("Error removing joystick with joystick id {joystick_id}: {err}"); 323 } 324 } 325 Event::JoyButtonDown { which: joystick_id, button_idx, .. } => { 326 if let Some(gamepad_idx) = joysticks.map_to_device_id(joystick_id) 327 && self.inputs.insert(GenericInput::Gamepad { 328 gamepad_idx, 329 action: GamepadAction::Button(button_idx), 330 }) == CollectionDone::Yes 331 { 332 return self.consume_collected_inputs(joysticks); 333 } 334 } 335 Event::JoyButtonUp { which: joystick_id, button_idx, .. } => { 336 if let Some(gamepad_idx) = joysticks.map_to_device_id(joystick_id) 337 && self.inputs.contains(GenericInput::Gamepad { 338 gamepad_idx, 339 action: GamepadAction::Button(button_idx), 340 }) 341 { 342 return self.consume_collected_inputs(joysticks); 343 } 344 } 345 Event::JoyAxisMotion { which: joystick_id, axis_idx, value, .. } => { 346 let gamepad_idx = joysticks.map_to_device_id(joystick_id)?; 347 348 let pressed = value.saturating_abs() > Self::AXIS_DEADZONE; 349 if pressed { 350 let direction = AxisDirection::from_value(value); 351 if self.inputs.insert(GenericInput::Gamepad { 352 gamepad_idx, 353 action: GamepadAction::Axis(axis_idx, direction), 354 }) == CollectionDone::Yes 355 { 356 return self.consume_collected_inputs(joysticks); 357 } 358 } else if self.inputs.axis_zero(gamepad_idx, axis_idx) == CollectionDone::Yes { 359 return self.consume_collected_inputs(joysticks); 360 } 361 } 362 Event::JoyHatMotion { which: joystick_id, hat_idx, state, .. } => { 363 let gamepad_idx = joysticks.map_to_device_id(joystick_id)?; 364 365 if state == HatState::Centered { 366 if HatDirection::ALL.into_iter().any(|direction| { 367 self.inputs.contains(GenericInput::Gamepad { 368 gamepad_idx, 369 action: GamepadAction::Hat(hat_idx, direction), 370 }) 371 }) { 372 return self.consume_collected_inputs(joysticks); 373 } 374 375 return None; 376 } 377 378 if let Some(direction) = hat_direction_for(state) 379 && self.inputs.insert(GenericInput::Gamepad { 380 gamepad_idx, 381 action: GamepadAction::Hat(hat_idx, direction), 382 }) == CollectionDone::Yes 383 { 384 return self.consume_collected_inputs(joysticks); 385 } 386 } 387 Event::MouseButtonDown { mouse_btn, window_id, .. } 388 if window_id == gui_window_id 389 && self.mouse_over_window 390 && self.inputs.insert(GenericInput::Mouse(mouse_btn)) 391 == CollectionDone::Yes => 392 { 393 return self.consume_collected_inputs(joysticks); 394 } 395 Event::MouseButtonUp { mouse_btn, window_id, .. } 396 if window_id == gui_window_id 397 && self.mouse_over_window 398 && self.inputs.contains(GenericInput::Mouse(mouse_btn)) => 399 { 400 return self.consume_collected_inputs(joysticks); 401 } 402 _ => {} 403 } 404 405 None 406 } 407 408 #[allow(clippy::unnecessary_wraps)] // Returns an Option for convenience of use 409 fn consume_collected_inputs(&mut self, joysticks: &Joysticks) -> Option<Vec<GenericInput>> { 410 let inputs = 411 mem::replace(&mut self.inputs, CollectedInputs::new(joysticks, Self::AXIS_DEADZONE)); 412 Some(inputs.consume()) 413 } 414} 415 416fn keyboard_input_for( 417 keycode: Option<Keycode>, 418 scancode: Option<Scancode>, 419) -> Option<KeyboardInput> { 420 // Prefer keycode (virtual key) over scancode (physical key location) if both are present, 421 // only using scancode if keycode is unknown (e.g. the ñ key on Spanish keyboards). 422 // This is mainly to respect the keyboard layout's modifier key locations, and to make the 423 // input configuration UI hopefully less confusing 424 match (keycode, scancode) { 425 (Some(keycode), _) => Some(KeyboardInput::Keycode(keycode)), 426 (None, Some(scancode)) => Some(KeyboardInput::Scancode(scancode)), 427 (None, None) => None, 428 } 429} 430 431fn hat_direction_for(state: HatState) -> Option<HatDirection> { 432 match state { 433 HatState::Up => Some(HatDirection::Up), 434 HatState::Left => Some(HatDirection::Left), 435 HatState::Right => Some(HatDirection::Right), 436 HatState::Down => Some(HatDirection::Down), 437 // Ignore diagonals for the purpose of collecting input 438 _ => None, 439 } 440} 441 442fn joystick_starting_state( 443 device_id: u32, 444 joystick: &Joystick, 445 axis_deadzone: i16, 446) -> impl Iterator<Item = GenericInput> + use<'_> { 447 buttons_starting_state(device_id, joystick) 448 .chain(axes_starting_state(device_id, joystick, axis_deadzone)) 449 .chain(hats_starting_state(device_id, joystick)) 450} 451 452fn buttons_starting_state( 453 gamepad_idx: u32, 454 joystick: &Joystick, 455) -> impl Iterator<Item = GenericInput> + use<'_> { 456 let num_buttons = joystick.num_buttons(); 457 log::debug!(" Gamepad {gamepad_idx} has {num_buttons} buttons"); 458 459 (0..num_buttons).filter_map(move |button_idx| { 460 let pressed = joystick.button(button_idx).ok()?; 461 log::debug!(" Button {button_idx} initial pressed: {pressed}"); 462 pressed.then_some(GenericInput::Gamepad { 463 gamepad_idx, 464 action: GamepadAction::Button(button_idx as u8), 465 }) 466 }) 467} 468 469fn axes_starting_state( 470 gamepad_idx: u32, 471 joystick: &Joystick, 472 deadzone: i16, 473) -> impl Iterator<Item = GenericInput> + use<'_> { 474 let num_axes = joystick.num_axes(); 475 log::debug!(" Gamepad {gamepad_idx} has {num_axes} axes"); 476 477 (0..num_axes).filter_map(move |axis_idx| { 478 let axis_value = joystick.axis(axis_idx).ok()?; 479 log::debug!(" Axis {axis_idx} initial value: {axis_value}"); 480 481 if axis_value.saturating_abs() < deadzone { 482 return None; 483 } 484 485 let direction = AxisDirection::from_value(axis_value); 486 Some(GenericInput::Gamepad { 487 gamepad_idx, 488 action: GamepadAction::Axis(axis_idx as u8, direction), 489 }) 490 }) 491} 492 493fn hats_starting_state( 494 gamepad_idx: u32, 495 joystick: &Joystick, 496) -> impl Iterator<Item = GenericInput> + use<'_> { 497 let num_hats = joystick.num_hats(); 498 log::debug!(" Gamepad {gamepad_idx} has {num_hats} hats"); 499 500 (0..num_hats).filter_map(move |hat_idx| { 501 let state = joystick.hat(hat_idx).ok()?; 502 log::debug!(" Hat {hat_idx} initial state: {state:?}"); 503 504 hat_direction_for(state).map(|hat_direction| GenericInput::Gamepad { 505 gamepad_idx, 506 action: GamepadAction::Hat(hat_idx as u8, hat_direction), 507 }) 508 }) 509} 510 511fn render_input_window(joysticks: &Joysticks, button: GenericButton, ui: &mut egui::Ui) { 512 egui::ScrollArea::vertical().show(ui, |ui| { 513 ui.horizontal(|ui| { 514 ui.label("Configuring button:"); 515 ui.label(RichText::new(button.label()).strong()); 516 }); 517 518 ui.add_space(10.0); 519 520 ui.label( 521 format!( 522 "Press a key, a gamepad input, or a mouse button. Mouse clicks must be on this window. Combinations of up to {} inputs simultaneously are supported.", 523 jgenesis_native_driver::input::MAX_MAPPING_LEN, 524 ) 525 ); 526 527 ui.add_space(10.0); 528 529 ui.label("Connected gamepads:"); 530 531 let devices: Vec<_> = joysticks.all_devices().collect(); 532 if devices.is_empty() { 533 ui.label(" (None)"); 534 } else { 535 for (gamepad_idx, (joystick, _)) in devices { 536 ui.label(format!(" Gamepad {gamepad_idx}: {}", joystick.name())); 537 } 538 } 539 }); 540}