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}