1mod helptext; 2 3use crate::app::inputcollect::InputCollectionState; 4use crate::app::widgets::NumericTextEdit; 5use crate::app::{App, OpenWindow}; 6use egui::{Button, Color32, ComboBox, Context, Grid, ScrollArea, Slider, Ui, Window}; 7use gb_config::GameBoyButton; 8use gba_config::GbaButton; 9use genesis_config::{GenesisButton, GenesisControllerType}; 10use jgenesis_common::input::Player; 11use jgenesis_native_config::genesis::GenesisAppConfig; 12use jgenesis_native_config::input::InputAppConfig; 13use jgenesis_native_config::input::mappings::{ 14 GameBoyInputMapping, GbaInputMapping, GbaJoypadMapping, GbaSolarMapping, 15 GenesisControllerMapping, GenesisInputMapping, HotkeyMapping, NesControllerMapping, 16 NesControllerType, NesInputMapping, NesZapperMapping, SmsGgControllerMapping, 17 SmsGgInputMapping, SnesControllerMapping, SnesControllerType, SnesInputMapping, 18}; 19use jgenesis_native_config::input::mappings::{PceInputMapping, PceJoypadMapping}; 20use jgenesis_native_config::input::{GenericInput, Hotkey}; 21use jgenesis_native_config::snes::SnesAppConfig; 22use nes_config::NesButton; 23use pce_config::{PceButton, PceInputDevice}; 24use polonius_the_crab::{polonius, polonius_return}; 25use smsgg_config::SmsGgButton; 26use snes_config::SnesButton; 27use std::mem; 28use std::sync::LazyLock; 29 30const ALLOW_OPPOSING_DIRECTIONS_LABEL: &str = "Allow simultaneous opposing gamepad directions"; 31 32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 33pub enum InputMappingSet { 34 #[default] 35 One, 36 Two, 37} 38 39impl InputMappingSet { 40 fn label(self) -> &'static str { 41 match self { 42 Self::One => "Input Mapping #1", 43 Self::Two => "Input Mapping #2", 44 } 45 } 46 47 fn smsgg(self, config: &mut InputAppConfig) -> &mut SmsGgInputMapping { 48 match self { 49 Self::One => &mut config.smsgg.mapping_1, 50 Self::Two => &mut config.smsgg.mapping_2, 51 } 52 } 53 54 fn genesis(self, config: &mut InputAppConfig) -> &mut GenesisInputMapping { 55 match self { 56 Self::One => &mut config.genesis.mapping_1, 57 Self::Two => &mut config.genesis.mapping_2, 58 } 59 } 60 61 fn nes(self, config: &mut InputAppConfig) -> &mut NesInputMapping { 62 match self { 63 Self::One => &mut config.nes.mapping_1, 64 Self::Two => &mut config.nes.mapping_2, 65 } 66 } 67 68 fn snes(self, config: &mut InputAppConfig) -> &mut SnesInputMapping { 69 match self { 70 Self::One => &mut config.snes.mapping_1, 71 Self::Two => &mut config.snes.mapping_2, 72 } 73 } 74 75 fn gb(self, config: &mut InputAppConfig, turbo: bool) -> &mut GameBoyInputMapping { 76 match (self, turbo) { 77 (Self::One, false) => &mut config.game_boy.mapping_1, 78 (Self::One, true) => &mut config.game_boy.mapping_1_turbo, 79 (Self::Two, false) => &mut config.game_boy.mapping_2, 80 (Self::Two, true) => &mut config.game_boy.mapping_2_turbo, 81 } 82 } 83 84 fn gba(self, config: &mut InputAppConfig, turbo: bool) -> &mut GbaInputMapping { 85 match (self, turbo) { 86 (Self::One, false) => &mut config.game_boy_advance.mapping_1, 87 (Self::One, true) => &mut config.game_boy_advance.mapping_1_turbo, 88 (Self::Two, false) => &mut config.game_boy_advance.mapping_2, 89 (Self::Two, true) => &mut config.game_boy_advance.mapping_2_turbo, 90 } 91 } 92 93 fn pce(self, config: &mut InputAppConfig) -> &mut PceInputMapping { 94 match self { 95 Self::One => &mut config.pc_engine.mapping_1, 96 Self::Two => &mut config.pc_engine.mapping_2, 97 } 98 } 99 100 fn hotkey(self, config: &mut InputAppConfig) -> &mut HotkeyMapping { 101 match self { 102 Self::One => &mut config.hotkeys.mapping_1, 103 Self::Two => &mut config.hotkeys.mapping_2, 104 } 105 } 106} 107 108#[derive(Debug, Clone, Copy, PartialEq, Eq)] 109pub enum GenericButton { 110 SmsGg(SmsGgButton, Player), 111 Genesis(GenesisButton, Player), 112 Nes(NesButton, Player), 113 Snes(SnesButton, Player), 114 GameBoy(GameBoyButton), 115 Gba(GbaButton), 116 Pce(PceButton, Player), 117 Hotkey(Hotkey), 118} 119 120impl GenericButton { 121 pub fn label(self) -> &'static str { 122 match self { 123 Self::SmsGg(button, _) => button.label(), 124 Self::Genesis(button, _) => button.label(), 125 Self::Nes(button, _) => button.label(), 126 Self::Snes(button, _) => button.label(), 127 Self::GameBoy(button) => button.label(), 128 Self::Gba(button) => button.label(), 129 Self::Pce(button, _) => button.label(), 130 Self::Hotkey(hotkey) => hotkey.label(), 131 } 132 } 133 134 pub fn access_value( 135 self, 136 mapping: InputMappingSet, 137 config: &mut InputAppConfig, 138 ) -> Option<&mut Option<Vec<GenericInput>>> { 139 match self { 140 Self::SmsGg(button, player) => { 141 access_smsgg_value(mapping, button, player, false, config) 142 } 143 Self::Genesis(button, player) => { 144 access_genesis_value(mapping, button, player, false, config) 145 } 146 Self::Nes(button, player) => access_nes_value(mapping, button, player, false, config), 147 Self::Snes(button, player) => access_snes_value(mapping, button, player, false, config), 148 Self::GameBoy(button) => access_gb_value(mapping, button, false, config), 149 Self::Gba(button) => access_gba_value(mapping, button, false, config), 150 Self::Pce(button, player) => access_pce_value(mapping, button, player, false, config), 151 Self::Hotkey(hotkey) => Some(access_hotkey(mapping, hotkey, config)), 152 } 153 } 154 155 pub fn access_value_turbo( 156 self, 157 mapping: InputMappingSet, 158 config: &mut InputAppConfig, 159 ) -> Option<&mut Option<Vec<GenericInput>>> { 160 match self { 161 Self::SmsGg(button @ (SmsGgButton::Button1 | SmsGgButton::Button2), player) => { 162 access_smsgg_value(mapping, button, player, true, config) 163 } 164 Self::Genesis( 165 button @ (GenesisButton::A 166 | GenesisButton::B 167 | GenesisButton::C 168 | GenesisButton::X 169 | GenesisButton::Y 170 | GenesisButton::Z 171 | GenesisButton::Xe1apA 172 | GenesisButton::Xe1apB 173 | GenesisButton::Xe1apC 174 | GenesisButton::Xe1apD 175 | GenesisButton::Xe1apE1 176 | GenesisButton::Xe1apE2 177 | GenesisButton::Xe1apAp 178 | GenesisButton::Xe1apBp), 179 player, 180 ) => access_genesis_value(mapping, button, player, true, config), 181 Self::Nes(button @ (NesButton::A | NesButton::B), player) => { 182 access_nes_value(mapping, button, player, true, config) 183 } 184 Self::Snes( 185 button @ (SnesButton::A 186 | SnesButton::B 187 | SnesButton::X 188 | SnesButton::Y 189 | SnesButton::L 190 | SnesButton::R), 191 player, 192 ) => access_snes_value(mapping, button, player, true, config), 193 Self::GameBoy(button @ (GameBoyButton::A | GameBoyButton::B)) => { 194 access_gb_value(mapping, button, true, config) 195 } 196 Self::Gba(button @ (GbaButton::A | GbaButton::B | GbaButton::L | GbaButton::R)) => { 197 access_gba_value(mapping, button, true, config) 198 } 199 Self::Pce(button @ (PceButton::Button1 | PceButton::Button2), player) => { 200 access_pce_value(mapping, button, player, true, config) 201 } 202 _ => None, 203 } 204 } 205 206 pub fn access_value_maybe_turbo( 207 self, 208 mapping: InputMappingSet, 209 config: &mut InputAppConfig, 210 turbo: bool, 211 ) -> Option<&mut Option<Vec<GenericInput>>> { 212 if turbo { 213 self.access_value_turbo(mapping, config) 214 } else { 215 self.access_value(mapping, config) 216 } 217 } 218} 219 220fn access_smsgg_value( 221 mapping: InputMappingSet, 222 button: SmsGgButton, 223 player: Player, 224 turbo: bool, 225 config: &mut InputAppConfig, 226) -> Option<&mut Option<Vec<GenericInput>>> { 227 let mapping_config = mapping.smsgg(config); 228 229 if button == SmsGgButton::Pause { 230 return Some(&mut mapping_config.pause); 231 } 232 233 let player_config = mapping_config.player_mapping(player, turbo)?; 234 player_config.access_value(button) 235} 236 237fn access_genesis_value( 238 mapping: InputMappingSet, 239 button: GenesisButton, 240 player: Player, 241 turbo: bool, 242 config: &mut InputAppConfig, 243) -> Option<&mut Option<Vec<GenericInput>>> { 244 let mapping_config = mapping.genesis(config); 245 let player_config = mapping_config.player_mapping(player, turbo)?; 246 player_config.access_value(button) 247} 248 249fn access_nes_value( 250 mapping: InputMappingSet, 251 button: NesButton, 252 player: Player, 253 turbo: bool, 254 config: &mut InputAppConfig, 255) -> Option<&mut Option<Vec<GenericInput>>> { 256 let mut mapping_config = mapping.nes(config); 257 258 polonius!(|mapping_config| -> Option<&'polonius mut Option<Vec<GenericInput>>> { 259 if let Some(value) = mapping_config.zapper.access_value(button) { 260 polonius_return!(Some(value)); 261 } 262 }); 263 264 let player_config = mapping_config.player_mapping(player, turbo)?; 265 player_config.access_value(button) 266} 267 268fn access_snes_value( 269 mapping: InputMappingSet, 270 button: SnesButton, 271 player: Player, 272 turbo: bool, 273 config: &mut InputAppConfig, 274) -> Option<&mut Option<Vec<GenericInput>>> { 275 let mapping_config = mapping.snes(config); 276 let player_config = mapping_config.player_mapping(player, turbo)?; 277 player_config.access_value(button) 278} 279 280fn access_gb_value( 281 mapping: InputMappingSet, 282 button: GameBoyButton, 283 turbo: bool, 284 config: &mut InputAppConfig, 285) -> Option<&mut Option<Vec<GenericInput>>> { 286 let mapping_config = mapping.gb(config, turbo); 287 mapping_config.access_value(button) 288} 289 290fn access_gba_value( 291 mapping: InputMappingSet, 292 button: GbaButton, 293 turbo: bool, 294 config: &mut InputAppConfig, 295) -> Option<&mut Option<Vec<GenericInput>>> { 296 let mapping_config = mapping.gba(config, turbo); 297 mapping_config.joypad.access_value(button).or_else(|| mapping_config.solar.access_value(button)) 298} 299 300fn access_pce_value( 301 mapping: InputMappingSet, 302 button: PceButton, 303 player: Player, 304 turbo: bool, 305 config: &mut InputAppConfig, 306) -> Option<&mut Option<Vec<GenericInput>>> { 307 let mapping_config = mapping.pce(config); 308 let player_config = mapping_config.player_mapping(player, turbo)?; 309 player_config.access_value(button) 310} 311 312fn access_hotkey( 313 mapping: InputMappingSet, 314 hotkey: Hotkey, 315 config: &mut InputAppConfig, 316) -> &mut Option<Vec<GenericInput>> { 317 let mapping_config = mapping.hotkey(config); 318 mapping_config.access_value(hotkey) 319} 320 321impl App { 322 pub(super) fn render_general_input_settings(&mut self, ctx: &Context) { 323 let mut open = true; 324 Window::new(OpenWindow::GeneralInput.title()).open(&mut open).show(ctx, |ui| { 325 ui.horizontal(|ui| { 326 ui.label("Gamepad joystick axis deadzone:"); 327 ui.add(Slider::new(&mut self.config.input.axis_deadzone, 0..=i16::MAX)); 328 329 if ui.button("Default").clicked() { 330 self.config.input.axis_deadzone = InputAppConfig::default().axis_deadzone; 331 } 332 }); 333 }); 334 if !open { 335 self.state.open_windows.remove(&OpenWindow::GeneralInput); 336 } 337 } 338 339 pub(super) fn render_smsgg_input_settings(&mut self, ctx: &Context) { 340 static P1_BUTTONS: LazyLock<Vec<GenericButton>> = LazyLock::new(|| { 341 SmsGgButton::ALL 342 .into_iter() 343 .filter_map(|button| { 344 (button != SmsGgButton::Pause) 345 .then_some(GenericButton::SmsGg(button, Player::One)) 346 }) 347 .collect() 348 }); 349 static P2_BUTTONS: LazyLock<Vec<GenericButton>> = LazyLock::new(|| { 350 SmsGgButton::ALL 351 .into_iter() 352 .filter_map(|button| { 353 (button != SmsGgButton::Pause) 354 .then_some(GenericButton::SmsGg(button, Player::Two)) 355 }) 356 .collect() 357 }); 358 359 let mut open = true; 360 Window::new(OpenWindow::SmsGgInput.title()).open(&mut open).show(ctx, |ui| { 361 self.disable_if_waiting_for_input(ui); 362 363 ui.checkbox( 364 &mut self.config.smsgg.allow_opposing_joypad_directions, 365 ALLOW_OPPOSING_DIRECTIONS_LABEL, 366 ); 367 ui.separator(); 368 369 let mapping = self.render_mapping_set_selector(OpenWindow::SmsGgInput, ui); 370 ui.separator(); 371 372 Grid::new("smsgg_inputs").spacing([50.0, 5.0]).show(ui, |ui| { 373 ui.heading("Player 1"); 374 ui.heading("Player 2"); 375 ui.end_row(); 376 377 self.render_input_buttons("smsgg_p1_input_settings", mapping, &P1_BUTTONS, ui); 378 self.render_input_buttons("smsgg_p2_input_settings", mapping, &P2_BUTTONS, ui); 379 ui.end_row(); 380 381 self.render_configure_all_button(&P1_BUTTONS, mapping, ui); 382 self.render_configure_all_button(&P2_BUTTONS, mapping, ui); 383 ui.end_row(); 384 }); 385 386 ui.add_space(15.0); 387 388 self.render_input_buttons( 389 "smsgg_pause_input", 390 mapping, 391 &[GenericButton::SmsGg(SmsGgButton::Pause, Player::One)], 392 ui, 393 ); 394 395 ui.add_space(15.0); 396 397 let mapping_config = mapping.smsgg(&mut self.config.input); 398 ui.horizontal(|ui| { 399 ComboBox::new("smsgg_presets", "").selected_text("Apply preset...").show_ui( 400 ui, 401 |ui| { 402 if ui.selectable_label(false, "Keyboard - Arrow movement").clicked() { 403 mapping_config.p1 = SmsGgControllerMapping::keyboard_arrows(); 404 mapping_config.p1_turbo = SmsGgControllerMapping::default(); 405 mapping_config.pause = SmsGgControllerMapping::keyboard_pause(); 406 } 407 408 if ui.selectable_label(false, "Keyboard - WASD movement").clicked() { 409 mapping_config.p1 = SmsGgControllerMapping::keyboard_wasd(); 410 mapping_config.p1_turbo = SmsGgControllerMapping::default(); 411 mapping_config.pause = SmsGgControllerMapping::keyboard_pause(); 412 } 413 }, 414 ); 415 416 if ui.button("Clear All P1").clicked() { 417 mapping_config.p1 = SmsGgControllerMapping::default(); 418 mapping_config.p1_turbo = SmsGgControllerMapping::default(); 419 } 420 421 if ui.button("Clear All P2").clicked() { 422 mapping_config.p2 = SmsGgControllerMapping::default(); 423 mapping_config.p2_turbo = SmsGgControllerMapping::default(); 424 } 425 }); 426 }); 427 if !open { 428 self.state.open_windows.remove(&OpenWindow::SmsGgInput); 429 } 430 } 431 432 pub(super) fn render_genesis_input_settings(&mut self, ctx: &Context) { 433 fn genesis_buttons( 434 player: Player, 435 filter: impl Fn(GenesisButton) -> bool, 436 ) -> Vec<GenericButton> { 437 GenesisButton::ALL 438 .into_iter() 439 .filter_map(|button| { 440 filter(button).then_some(GenericButton::Genesis(button, player)) 441 }) 442 .collect() 443 } 444 445 static P1_GAMEPAD: LazyLock<Vec<GenericButton>> = 446 LazyLock::new(|| genesis_buttons(Player::One, GenesisButton::is_gamepad)); 447 static P2_GAMEPAD: LazyLock<Vec<GenericButton>> = 448 LazyLock::new(|| genesis_buttons(Player::Two, GenesisButton::is_gamepad)); 449 static P1_MOUSE: LazyLock<Vec<GenericButton>> = 450 LazyLock::new(|| genesis_buttons(Player::One, GenesisButton::is_mouse)); 451 static P2_MOUSE: LazyLock<Vec<GenericButton>> = 452 LazyLock::new(|| genesis_buttons(Player::Two, GenesisButton::is_mouse)); 453 static P1_XE1AP: LazyLock<Vec<GenericButton>> = 454 LazyLock::new(|| genesis_buttons(Player::One, GenesisButton::is_xe1ap)); 455 static P2_XE1AP: LazyLock<Vec<GenericButton>> = 456 LazyLock::new(|| genesis_buttons(Player::Two, GenesisButton::is_xe1ap)); 457 458 let mut open = true; 459 Window::new(OpenWindow::GenesisInput.title()).open(&mut open).show(ctx, |ui| { 460 self.disable_if_waiting_for_input(ui); 461 462 ui.horizontal(|ui| { 463 for (label, controller_type_field) in [ 464 ("Player 1 controller type", &mut self.config.input.genesis.p1_type), 465 ("Player 2 controller type", &mut self.config.input.genesis.p2_type), 466 ] { 467 ui.vertical(|ui| { 468 ui.group(|ui| { 469 ui.label(label); 470 471 ui.horizontal(|ui| { 472 for (value, label) in [ 473 (GenesisControllerType::ThreeButton, "3-button"), 474 (GenesisControllerType::SixButton, "6-button"), 475 (GenesisControllerType::MegaMouse, "Mega Mouse"), 476 (GenesisControllerType::Xe1ap, "XE-1 AP"), 477 (GenesisControllerType::None, "None"), 478 ] { 479 ui.radio_value(controller_type_field, value, label); 480 } 481 }); 482 }); 483 }); 484 } 485 }); 486 487 ui.add_space(3.0); 488 489 ui.checkbox( 490 &mut self.config.genesis.allow_opposing_joypad_directions, 491 ALLOW_OPPOSING_DIRECTIONS_LABEL, 492 ); 493 ui.checkbox( 494 &mut self.config.genesis.auto_3_button_mode, 495 "Automatically force 3-button mode in 6-button-incompatible games", 496 ); 497 498 ui.add_space(3.0); 499 500 ui.horizontal(|ui| { 501 ui.label("Mega Mouse sensitivity:"); 502 ui.add(Slider::new(&mut self.config.genesis.mega_mouse_sensitivity, 0.0..=10.0)); 503 504 if ui.button("Default").clicked() { 505 self.config.genesis.mega_mouse_sensitivity = 506 GenesisAppConfig::default().mega_mouse_sensitivity; 507 } 508 }); 509 510 ui.separator(); 511 512 let mapping = self.render_mapping_set_selector(OpenWindow::GenesisInput, ui); 513 ui.separator(); 514 515 let p1_type = self.config.input.genesis.p1_type; 516 let p2_type = self.config.input.genesis.p2_type; 517 518 Grid::new("genesis_inputs").spacing([50.0, 5.0]).show(ui, |ui| { 519 let (p1_heading, p1_buttons) = match p1_type { 520 GenesisControllerType::ThreeButton 521 | GenesisControllerType::SixButton 522 | GenesisControllerType::None => ("Player 1 - Gamepad", &P1_GAMEPAD), 523 GenesisControllerType::MegaMouse => ("Player 1 - Mega Mouse", &P1_MOUSE), 524 GenesisControllerType::Xe1ap => ("Player 1 - XE-1 AP", &P1_XE1AP), 525 }; 526 let (p2_heading, p2_buttons) = match p2_type { 527 GenesisControllerType::ThreeButton 528 | GenesisControllerType::SixButton 529 | GenesisControllerType::None => ("Player 2 - Gamepad", &P2_GAMEPAD), 530 GenesisControllerType::MegaMouse => ("Player 2 - Mega Mouse", &P2_MOUSE), 531 GenesisControllerType::Xe1ap => ("Player 2 - XE-1 AP", &P2_XE1AP), 532 }; 533 534 ui.heading(p1_heading); 535 ui.heading(p2_heading); 536 ui.end_row(); 537 538 self.render_input_buttons("genesis_p1_input_settings", mapping, p1_buttons, ui); 539 self.render_input_buttons("genesis_p2_input_settings", mapping, p2_buttons, ui); 540 ui.end_row(); 541 542 self.render_configure_all_button(p1_buttons, mapping, ui); 543 self.render_configure_all_button(p2_buttons, mapping, ui); 544 ui.end_row(); 545 }); 546 547 ui.add_space(15.0); 548 549 let mapping_config = mapping.genesis(&mut self.config.input); 550 ui.horizontal(|ui| { 551 ui.add_enabled_ui(p1_type != GenesisControllerType::Xe1ap, |ui| { 552 ComboBox::new("genesis_presets", "").selected_text("Apply preset...").show_ui( 553 ui, 554 |ui| { 555 if ui.selectable_label(false, "Keyboard - Arrow movement").clicked() { 556 mapping_config.p1.clone_from_type( 557 &GenesisControllerMapping::keyboard_arrows(), 558 p1_type, 559 ); 560 mapping_config 561 .p1_turbo 562 .clone_from_type(&GenesisControllerMapping::default(), p1_type); 563 } 564 565 if ui.selectable_label(false, "Keyboard - WASD movement").clicked() { 566 mapping_config.p1.clone_from_type( 567 &GenesisControllerMapping::keyboard_wasd(), 568 p1_type, 569 ); 570 mapping_config 571 .p1_turbo 572 .clone_from_type(&GenesisControllerMapping::default(), p1_type); 573 } 574 }, 575 ); 576 }); 577 578 if ui.button("Clear All P1").clicked() { 579 mapping_config 580 .p1 581 .clone_from_type(&GenesisControllerMapping::default(), p1_type); 582 mapping_config 583 .p1_turbo 584 .clone_from_type(&GenesisControllerMapping::default(), p1_type); 585 } 586 587 if ui.button("Clear All P2").clicked() { 588 mapping_config 589 .p2 590 .clone_from_type(&GenesisControllerMapping::default(), p2_type); 591 mapping_config 592 .p2_turbo 593 .clone_from_type(&GenesisControllerMapping::default(), p2_type); 594 } 595 }); 596 }); 597 if !open { 598 self.state.open_windows.remove(&OpenWindow::GenesisInput); 599 } 600 } 601 602 pub(super) fn render_nes_input_settings(&mut self, ctx: &Context) { 603 static P1_BUTTONS: LazyLock<Vec<GenericButton>> = LazyLock::new(|| { 604 NesButton::ALL 605 .into_iter() 606 .filter_map(|button| { 607 (!button.is_zapper()).then_some(GenericButton::Nes(button, Player::One)) 608 }) 609 .collect() 610 }); 611 static P2_BUTTONS: LazyLock<Vec<GenericButton>> = LazyLock::new(|| { 612 NesButton::ALL 613 .into_iter() 614 .filter_map(|button| { 615 (!button.is_zapper()).then_some(GenericButton::Nes(button, Player::Two)) 616 }) 617 .collect() 618 }); 619 620 let mut open = true; 621 Window::new(OpenWindow::NesInput.title()).open(&mut open).show(ctx, |ui| { 622 self.disable_if_waiting_for_input(ui); 623 624 let rect = ui 625 .checkbox( 626 &mut self.config.nes.allow_opposing_joypad_directions, 627 ALLOW_OPPOSING_DIRECTIONS_LABEL, 628 ) 629 .interact_rect; 630 if ui.rect_contains_pointer(rect) { 631 self.state 632 .help_text 633 .insert(OpenWindow::NesInput, helptext::NES_OPPOSING_JOYPAD_DIRECTIONS); 634 } 635 ui.separator(); 636 637 let mapping = self.render_mapping_set_selector(OpenWindow::NesInput, ui); 638 ui.separator(); 639 640 Grid::new("nes_inputs").spacing([50.0, 5.0]).show(ui, |ui| { 641 ui.heading("Player 1"); 642 ui.heading("Player 2"); 643 ui.end_row(); 644 645 self.render_input_buttons("nes_p1_inputs", mapping, &P1_BUTTONS, ui); 646 self.render_input_buttons("nes_p2_inputs", mapping, &P2_BUTTONS, ui); 647 ui.end_row(); 648 649 self.render_configure_all_button(&P1_BUTTONS, mapping, ui); 650 self.render_configure_all_button(&P2_BUTTONS, mapping, ui); 651 ui.end_row(); 652 }); 653 654 ui.add_space(15.0); 655 656 let mapping_config = mapping.nes(&mut self.config.input); 657 ui.horizontal(|ui| { 658 ComboBox::new("nes_presets", "").selected_text("Apply preset...").show_ui( 659 ui, 660 |ui| { 661 if ui.selectable_label(false, "Keyboard - Arrow movement").clicked() { 662 mapping_config.p1 = NesControllerMapping::keyboard_arrows(); 663 mapping_config.p1_turbo = NesControllerMapping::default(); 664 } 665 666 if ui.selectable_label(false, "Keyboard - WASD movement").clicked() { 667 mapping_config.p1 = NesControllerMapping::keyboard_wasd(); 668 mapping_config.p1_turbo = NesControllerMapping::default(); 669 } 670 }, 671 ); 672 673 if ui.button("Clear All P1").clicked() { 674 mapping_config.p1 = NesControllerMapping::default(); 675 mapping_config.p1_turbo = NesControllerMapping::default(); 676 } 677 678 if ui.button("Clear All P2").clicked() { 679 mapping_config.p2 = NesControllerMapping::default(); 680 mapping_config.p2_turbo = NesControllerMapping::default(); 681 } 682 }); 683 684 self.render_help_text(ui, OpenWindow::NesInput); 685 }); 686 if !open { 687 self.state.open_windows.remove(&OpenWindow::NesInput); 688 } 689 } 690 691 pub(super) fn render_nes_peripheral_settings(&mut self, ctx: &Context) { 692 static ZAPPER_BUTTONS: LazyLock<Vec<GenericButton>> = LazyLock::new(|| { 693 NesButton::ALL 694 .into_iter() 695 .filter_map(|button| { 696 button.is_zapper().then_some(GenericButton::Nes(button, Player::One)) 697 }) 698 .collect() 699 }); 700 701 let mut open = true; 702 Window::new(OpenWindow::NesPeripherals.title()).open(&mut open).show(ctx, |ui| { 703 self.disable_if_waiting_for_input(ui); 704 705 ui.group(|ui| { 706 ui.label("Player 2 device"); 707 708 ui.horizontal(|ui| { 709 ui.radio_value( 710 &mut self.config.input.nes.p2_type, 711 NesControllerType::Gamepad, 712 "Gamepad", 713 ); 714 ui.radio_value( 715 &mut self.config.input.nes.p2_type, 716 NesControllerType::Zapper, 717 "Zapper", 718 ); 719 }); 720 }); 721 722 ui.separator(); 723 let mapping = self.render_mapping_set_selector(OpenWindow::NesPeripherals, ui); 724 ui.separator(); 725 726 ui.heading("Zapper"); 727 728 ui.add_space(5.0); 729 730 self.render_input_buttons("nes_zapper_inputs", mapping, &ZAPPER_BUTTONS, ui); 731 732 ui.add_space(15.0); 733 734 let mapping_config = mapping.nes(&mut self.config.input); 735 ui.horizontal(|ui| { 736 if ui.button("Restore Defaults").clicked() { 737 mapping_config.zapper = NesZapperMapping::mouse(); 738 } 739 740 if ui.button("Clear All").clicked() { 741 mapping_config.zapper = NesZapperMapping::default(); 742 } 743 }); 744 }); 745 if !open { 746 self.state.open_windows.remove(&OpenWindow::NesPeripherals); 747 } 748 } 749 750 pub(super) fn render_snes_input_settings(&mut self, ctx: &Context) { 751 fn snes_buttons(player: Player, filter: fn(SnesButton) -> bool) -> Vec<GenericButton> { 752 SnesButton::ALL 753 .into_iter() 754 .filter_map(|button| filter(button).then_some(GenericButton::Snes(button, player))) 755 .collect() 756 } 757 758 static P1_GAMEPAD_BUTTONS: LazyLock<Vec<GenericButton>> = 759 LazyLock::new(|| snes_buttons(Player::One, SnesButton::is_gamepad)); 760 static P2_GAMEPAD_BUTTONS: LazyLock<Vec<GenericButton>> = 761 LazyLock::new(|| snes_buttons(Player::Two, SnesButton::is_gamepad)); 762 static P1_MOUSE_BUTTONS: LazyLock<Vec<GenericButton>> = 763 LazyLock::new(|| snes_buttons(Player::One, SnesButton::is_mouse)); 764 static P2_MOUSE_BUTTONS: LazyLock<Vec<GenericButton>> = 765 LazyLock::new(|| snes_buttons(Player::Two, SnesButton::is_mouse)); 766 static P1_SUPER_SCOPE_BUTTONS: LazyLock<Vec<GenericButton>> = 767 LazyLock::new(|| snes_buttons(Player::One, SnesButton::is_super_scope)); 768 static P2_SUPER_SCOPE_BUTTONS: LazyLock<Vec<GenericButton>> = 769 LazyLock::new(|| snes_buttons(Player::Two, SnesButton::is_super_scope)); 770 771 let mut open = true; 772 Window::new(OpenWindow::SnesInput.title()).open(&mut open).show(ctx, |ui| { 773 self.disable_if_waiting_for_input(ui); 774 775 ui.horizontal(|ui| { 776 for (heading, field) in [ 777 ("Player 1 controller type", &mut self.config.input.snes.p1_type), 778 ("Player 2 controller type", &mut self.config.input.snes.p2_type), 779 ] { 780 ui.group(|ui| { 781 ui.vertical(|ui| { 782 ui.label(heading); 783 784 ui.horizontal(|ui| { 785 for (label, value) in [ 786 ("Gamepad", SnesControllerType::Gamepad), 787 ("Mouse", SnesControllerType::Mouse), 788 ("Super Scope", SnesControllerType::SuperScope), 789 ("None", SnesControllerType::None), 790 ] { 791 ui.radio_value(field, value, label); 792 } 793 }); 794 }); 795 }); 796 } 797 }); 798 799 if self.config.input.snes.p1_type == SnesControllerType::SuperScope { 800 ui.colored_label( 801 Color32::RED, 802 "Super Scope only works properly in controller port 2", 803 ); 804 } 805 806 ui.add_space(3.0); 807 808 ui.checkbox( 809 &mut self.config.snes.allow_opposing_joypad_directions, 810 ALLOW_OPPOSING_DIRECTIONS_LABEL, 811 ); 812 813 ui.add_space(3.0); 814 815 ui.horizontal(|ui| { 816 ui.label("Mouse sensitivity:"); 817 ui.add(Slider::new(&mut self.config.snes.mouse_sensitivity, 0.0..=10.0)); 818 819 if ui.button("Default").clicked() { 820 self.config.snes.mouse_sensitivity = SnesAppConfig::default().mouse_sensitivity; 821 } 822 }); 823 824 ui.separator(); 825 826 let mapping = self.render_mapping_set_selector(OpenWindow::SnesInput, ui); 827 ui.separator(); 828 829 let (p1_heading, p1_buttons) = match self.config.input.snes.p1_type { 830 SnesControllerType::Gamepad | SnesControllerType::None => { 831 ("Player 1 - Gamepad", &P1_GAMEPAD_BUTTONS) 832 } 833 SnesControllerType::Mouse => ("Player 1 - Mouse", &P1_MOUSE_BUTTONS), 834 SnesControllerType::SuperScope => { 835 ("Player 1 - Super Scope", &P1_SUPER_SCOPE_BUTTONS) 836 } 837 }; 838 let (p2_heading, p2_buttons) = match self.config.input.snes.p2_type { 839 SnesControllerType::Gamepad | SnesControllerType::None => { 840 ("Player 2 - Gamepad", &P2_GAMEPAD_BUTTONS) 841 } 842 SnesControllerType::Mouse => ("Player 2 - Mouse", &P2_MOUSE_BUTTONS), 843 SnesControllerType::SuperScope => { 844 ("Player 2 - Super Scope", &P2_SUPER_SCOPE_BUTTONS) 845 } 846 }; 847 848 Grid::new("snes_inputs").spacing([50.0, 5.0]).show(ui, |ui| { 849 ui.heading(p1_heading); 850 ui.heading(p2_heading); 851 ui.end_row(); 852 853 self.render_input_buttons("snes_p1_inputs", mapping, p1_buttons, ui); 854 self.render_input_buttons("snes_p2_inputs", mapping, p2_buttons, ui); 855 ui.end_row(); 856 857 self.render_configure_all_button(p1_buttons, mapping, ui); 858 self.render_configure_all_button(p2_buttons, mapping, ui); 859 ui.end_row(); 860 }); 861 862 ui.add_space(15.0); 863 864 let mapping_config = mapping.snes(&mut self.config.input); 865 ui.horizontal(|ui| { 866 ComboBox::new("snes_presets", "").selected_text("Apply preset...").show_ui( 867 ui, 868 |ui| { 869 if ui.selectable_label(false, "Keyboard - Arrow movement").clicked() { 870 mapping_config.p1 = SnesControllerMapping::keyboard_arrows(); 871 mapping_config.p1_turbo = SnesControllerMapping::default(); 872 } 873 874 if ui.selectable_label(false, "Keyboard - WASD movement").clicked() { 875 mapping_config.p1 = SnesControllerMapping::keyboard_wasd(); 876 mapping_config.p1_turbo = SnesControllerMapping::default(); 877 } 878 }, 879 ); 880 881 if ui.button("Clear All P1").clicked() { 882 mapping_config.p1 = SnesControllerMapping::default(); 883 mapping_config.p1_turbo = SnesControllerMapping::default(); 884 } 885 886 if ui.button("Clear All P2").clicked() { 887 mapping_config.p2 = SnesControllerMapping::default(); 888 mapping_config.p2_turbo = SnesControllerMapping::default(); 889 } 890 }); 891 }); 892 if !open { 893 self.state.open_windows.remove(&OpenWindow::SnesInput); 894 } 895 } 896 897 pub(super) fn render_gb_input_settings(&mut self, ctx: &Context) { 898 static BUTTONS: LazyLock<Vec<GenericButton>> = 899 LazyLock::new(|| GameBoyButton::ALL.into_iter().map(GenericButton::GameBoy).collect()); 900 901 let mut open = true; 902 Window::new(OpenWindow::GameBoyInput.title()).open(&mut open).show(ctx, |ui| { 903 self.disable_if_waiting_for_input(ui); 904 905 ui.checkbox( 906 &mut self.config.game_boy.allow_opposing_joypad_directions, 907 ALLOW_OPPOSING_DIRECTIONS_LABEL, 908 ); 909 ui.separator(); 910 911 let mapping = self.render_mapping_set_selector(OpenWindow::GameBoyInput, ui); 912 ui.separator(); 913 914 self.render_input_buttons("gb_inputs", mapping, &BUTTONS, ui); 915 916 self.render_configure_all_button(&BUTTONS, mapping, ui); 917 918 ui.add_space(15.0); 919 920 ui.horizontal(|ui| { 921 ComboBox::new("gb_presets", "").selected_text("Apply preset...").show_ui( 922 ui, 923 |ui| { 924 if ui.selectable_label(false, "Keyboard - Arrow movement").clicked() { 925 *mapping.gb(&mut self.config.input, false) = 926 GameBoyInputMapping::keyboard_arrows(); 927 *mapping.gb(&mut self.config.input, true) = 928 GameBoyInputMapping::default(); 929 } 930 931 if ui.selectable_label(false, "Keyboard - WASD movement").clicked() { 932 *mapping.gb(&mut self.config.input, false) = 933 GameBoyInputMapping::keyboard_wasd(); 934 *mapping.gb(&mut self.config.input, true) = 935 GameBoyInputMapping::default(); 936 } 937 }, 938 ); 939 940 if ui.button("Clear All").clicked() { 941 *mapping.gb(&mut self.config.input, false) = GameBoyInputMapping::default(); 942 *mapping.gb(&mut self.config.input, true) = GameBoyInputMapping::default(); 943 } 944 }); 945 }); 946 if !open { 947 self.state.open_windows.remove(&OpenWindow::GameBoyInput); 948 } 949 } 950 951 pub(super) fn render_gba_input_settings(&mut self, ctx: &Context) { 952 static BUTTONS: LazyLock<Vec<GenericButton>> = LazyLock::new(|| { 953 GbaButton::ALL 954 .into_iter() 955 .filter_map(|button| button.is_joypad().then_some(GenericButton::Gba(button))) 956 .collect() 957 }); 958 959 let mut open = true; 960 Window::new(OpenWindow::GbaInput.title()).open(&mut open).show(ctx, |ui| { 961 self.disable_if_waiting_for_input(ui); 962 963 ui.checkbox( 964 &mut self.config.game_boy_advance.allow_opposing_joypad_directions, 965 ALLOW_OPPOSING_DIRECTIONS_LABEL, 966 ); 967 ui.separator(); 968 969 let mapping = self.render_mapping_set_selector(OpenWindow::GbaInput, ui); 970 ui.separator(); 971 972 self.render_input_buttons("gba_inputs", mapping, &BUTTONS, ui); 973 974 self.render_configure_all_button(&BUTTONS, mapping, ui); 975 976 ui.add_space(15.0); 977 978 ui.horizontal(|ui| { 979 ComboBox::new("gba_presets", "").selected_text("Apply preset...").show_ui( 980 ui, 981 |ui| { 982 if ui.selectable_label(false, "Keyboard - Arrow movement").clicked() { 983 mapping.gba(&mut self.config.input, false).joypad = 984 GbaJoypadMapping::keyboard_arrows(); 985 mapping.gba(&mut self.config.input, true).joypad = 986 GbaJoypadMapping::default(); 987 } 988 989 if ui.selectable_label(false, "Keyboard - WASD movement").clicked() { 990 mapping.gba(&mut self.config.input, false).joypad = 991 GbaJoypadMapping::keyboard_wasd(); 992 mapping.gba(&mut self.config.input, true).joypad = 993 GbaJoypadMapping::default(); 994 } 995 }, 996 ); 997 998 if ui.button("Clear All").clicked() { 999 mapping.gba(&mut self.config.input, false).joypad = GbaJoypadMapping::default(); 1000 mapping.gba(&mut self.config.input, true).joypad = GbaJoypadMapping::default(); 1001 } 1002 }); 1003 }); 1004 if !open { 1005 self.state.open_windows.remove(&OpenWindow::GbaInput); 1006 } 1007 } 1008 1009 pub(super) fn render_gba_peripheral_settings(&mut self, ctx: &Context) { 1010 const WINDOW: OpenWindow = OpenWindow::GbaPeripherals; 1011 1012 static SOLAR_BUTTONS: LazyLock<Vec<GenericButton>> = LazyLock::new(|| { 1013 GbaButton::ALL 1014 .into_iter() 1015 .filter_map(|button| button.is_solar_sensor().then_some(GenericButton::Gba(button))) 1016 .collect() 1017 }); 1018 1019 let mut open = true; 1020 Window::new(OpenWindow::GbaPeripherals.title()).open(&mut open).show(ctx, |ui| { 1021 self.disable_if_waiting_for_input(ui); 1022 1023 let mapping = self.render_mapping_set_selector(WINDOW, ui); 1024 ui.separator(); 1025 1026 ui.heading("Solar Sensor"); 1027 1028 self.render_input_buttons("solar_sensor_inputs", mapping, &SOLAR_BUTTONS, ui); 1029 1030 ui.add_space(15.0); 1031 1032 let mapping_config = mapping.gba(&mut self.config.input, false); 1033 if ui.button("Clear All").clicked() { 1034 mapping_config.solar = GbaSolarMapping::default(); 1035 } 1036 1037 ui.add_space(15.0); 1038 1039 let prev_slider_width = mem::replace(&mut ui.style_mut().spacing.slider_width, 200.0); 1040 1041 Grid::new("gba_solar_grid").show(ui, |ui| { 1042 ui.label("Brightness step"); 1043 1044 ui.add(Slider::new( 1045 &mut self.config.game_boy_advance.solar_brightness_step, 1046 1..=255, 1047 )); 1048 1049 if ui.button("Default").clicked() { 1050 self.config.game_boy_advance.solar_brightness_step = 1051 gba_config::DEFAULT_SOLAR_BRIGHTNESS_STEP; 1052 } 1053 1054 ui.end_row(); 1055 1056 ui.label("Minimum brightness"); 1057 1058 ui.add(Slider::new( 1059 &mut self.config.game_boy_advance.solar_min_brightness, 1060 0..=255, 1061 )); 1062 1063 if ui.button("Default").clicked() { 1064 self.config.game_boy_advance.solar_min_brightness = 1065 gba_config::DEFAULT_SOLAR_MIN_BRIGHTNESS; 1066 } 1067 1068 ui.end_row(); 1069 1070 ui.label("Maximum brightness"); 1071 1072 ui.add(Slider::new( 1073 &mut self.config.game_boy_advance.solar_max_brightness, 1074 0..=255, 1075 )); 1076 1077 if ui.button("Default").clicked() { 1078 self.config.game_boy_advance.solar_max_brightness = 1079 gba_config::DEFAULT_SOLAR_MAX_BRIGHTNESS; 1080 } 1081 1082 ui.end_row(); 1083 }); 1084 1085 ui.style_mut().spacing.slider_width = prev_slider_width; 1086 }); 1087 1088 if !open { 1089 self.state.open_windows.remove(&OpenWindow::GbaPeripherals); 1090 } 1091 } 1092 1093 pub(super) fn render_pce_input_settings(&mut self, ctx: &Context) { 1094 let mut open = true; 1095 Window::new(OpenWindow::PceInput.title()).open(&mut open).show(ctx, |ui| { 1096 self.disable_if_waiting_for_input(ui); 1097 1098 ui.group(|ui| { 1099 ui.label("Controller port"); 1100 1101 ui.horizontal(|ui| { 1102 for (value, label) in [ 1103 (PceInputDevice::TwoButtonGamepad, "Gamepad"), 1104 (PceInputDevice::TurboTap, "Turbo Tap"), 1105 ] { 1106 ui.radio_value(&mut self.config.pc_engine.input_device, value, label); 1107 } 1108 }); 1109 1110 ui.horizontal(|ui| { 1111 ui.add_enabled_ui( 1112 self.config.pc_engine.input_device == PceInputDevice::TurboTap, 1113 |ui| { 1114 ui.label("Turbo Tap gamepads:"); 1115 1116 for (idx, connected) in 1117 self.config.pc_engine.turbo_tap_connected.iter_mut().enumerate() 1118 { 1119 ui.checkbox(connected, (idx + 1).to_string()); 1120 } 1121 }, 1122 ); 1123 }); 1124 }); 1125 1126 let rect = ui 1127 .checkbox( 1128 &mut self.config.pc_engine.allow_opposing_joypad_directions, 1129 ALLOW_OPPOSING_DIRECTIONS_LABEL, 1130 ) 1131 .interact_rect; 1132 if ui.rect_contains_pointer(rect) { 1133 self.state 1134 .help_text 1135 .insert(OpenWindow::PceInput, helptext::OPPOSING_JOYPAD_DIRECTIONS); 1136 } 1137 1138 let rect = ui 1139 .checkbox( 1140 &mut self.config.pc_engine.allow_simultaneous_run_select, 1141 "Allow pressing Run+Select simultaneously", 1142 ) 1143 .interact_rect; 1144 if ui.rect_contains_pointer(rect) { 1145 self.state 1146 .help_text 1147 .insert(OpenWindow::PceInput, helptext::PCE_SIMULTANEOUS_RUN_SELECT); 1148 } 1149 1150 ui.separator(); 1151 1152 let mapping = self.render_mapping_set_selector(OpenWindow::PceInput, ui); 1153 ui.separator(); 1154 1155 let using_turbo_tap = self.config.pc_engine.input_device == PceInputDevice::TurboTap; 1156 if !using_turbo_tap { 1157 self.state.pce_selected_player_idx = 0; 1158 } 1159 1160 ui.add_enabled_ui(using_turbo_tap, |ui| { 1161 ComboBox::new("pce_player_combo", "").show_index( 1162 ui, 1163 &mut self.state.pce_selected_player_idx, 1164 5, 1165 |player_idx| format!("Player {}", player_idx + 1), 1166 ); 1167 }); 1168 ui.add_space(5.0); 1169 1170 let player = [Player::One, Player::Two, Player::Three, Player::Four, Player::Five] 1171 [self.state.pce_selected_player_idx]; 1172 1173 Grid::new("pce_inputs").spacing([50.0, 5.0]).show(ui, |ui| { 1174 let buttons = PceButton::ALL 1175 .into_iter() 1176 .map(|button| GenericButton::Pce(button, player)) 1177 .collect::<Vec<_>>(); 1178 1179 self.render_input_buttons("pce_p1_inputs", mapping, &buttons, ui); 1180 ui.end_row(); 1181 1182 self.render_configure_all_button(&buttons, mapping, ui); 1183 ui.end_row(); 1184 }); 1185 1186 ui.add_space(15.0); 1187 1188 let mapping_config = mapping.pce(&mut self.config.input); 1189 let (mapping, turbo_mapping) = match player { 1190 Player::One => (&mut mapping_config.p1, &mut mapping_config.p1_turbo), 1191 Player::Two => (&mut mapping_config.p2, &mut mapping_config.p2_turbo), 1192 Player::Three => (&mut mapping_config.p3, &mut mapping_config.p3_turbo), 1193 Player::Four => (&mut mapping_config.p4, &mut mapping_config.p4_turbo), 1194 Player::Five => (&mut mapping_config.p5, &mut mapping_config.p5_turbo), 1195 _ => unreachable!("player is always 1-5"), 1196 }; 1197 1198 ui.horizontal(|ui| { 1199 ComboBox::new("pce_presets", "").selected_text("Apply preset...").show_ui( 1200 ui, 1201 |ui| { 1202 if ui.selectable_label(false, "Keyboard - Arrow movement").clicked() { 1203 *mapping = PceJoypadMapping::keyboard_arrows(); 1204 *turbo_mapping = PceJoypadMapping::default(); 1205 } 1206 1207 if ui.selectable_label(false, "Keyboard - WASD movement").clicked() { 1208 *mapping = PceJoypadMapping::keyboard_wasd(); 1209 *turbo_mapping = PceJoypadMapping::default(); 1210 } 1211 }, 1212 ); 1213 1214 if ui.button("Clear All").clicked() { 1215 *mapping = PceJoypadMapping::default(); 1216 *turbo_mapping = PceJoypadMapping::default(); 1217 } 1218 }); 1219 1220 self.render_help_text(ui, OpenWindow::PceInput); 1221 }); 1222 if !open { 1223 self.state.open_windows.remove(&OpenWindow::PceInput); 1224 } 1225 } 1226 1227 pub(super) fn render_hotkey_settings(&mut self, ctx: &Context) { 1228 static GENERAL_HOTKEYS: LazyLock<Vec<GenericButton>> = 1229 LazyLock::new(|| hotkey_vec(HotkeyCategory::General)); 1230 static STATE_HOTKEYS: LazyLock<Vec<GenericButton>> = 1231 LazyLock::new(|| hotkey_vec(HotkeyCategory::SaveState)); 1232 1233 let mut open = true; 1234 Window::new(OpenWindow::Hotkeys.title()).open(&mut open).show(ctx, |ui| { 1235 self.disable_if_waiting_for_input(ui); 1236 1237 let mapping = self.render_mapping_set_selector(OpenWindow::Hotkeys, ui); 1238 ui.separator(); 1239 1240 ScrollArea::vertical() 1241 .auto_shrink([false, true]) 1242 .max_height(ctx.content_rect().height() * 0.5) 1243 .show(ui, |ui| { 1244 ui.heading("General"); 1245 self.render_input_buttons("general_hotkeys", mapping, &GENERAL_HOTKEYS, ui); 1246 1247 ui.separator(); 1248 1249 ui.heading("Save States"); 1250 self.render_input_buttons("state_hotkeys", mapping, &STATE_HOTKEYS, ui); 1251 }); 1252 1253 ui.add_space(15.0); 1254 1255 let mapping_config = mapping.hotkey(&mut self.config.input); 1256 ui.horizontal(|ui| { 1257 if ui.button("Restore Defaults").clicked() { 1258 *mapping_config = HotkeyMapping::default_keyboard(); 1259 } 1260 1261 if ui.button("Clear All").clicked() { 1262 *mapping_config = HotkeyMapping::default(); 1263 } 1264 }); 1265 1266 ui.separator(); 1267 1268 ui.horizontal(|ui| { 1269 ui.add( 1270 NumericTextEdit::new( 1271 &mut self.state.ff_multiplier_text, 1272 &mut self.config.common.fast_forward_multiplier, 1273 &mut self.state.ff_multiplier_invalid, 1274 ) 1275 .with_validation(|value| value != 0) 1276 .desired_width(30.0), 1277 ); 1278 1279 ui.label("Fast forward multiplier"); 1280 }); 1281 if self.state.ff_multiplier_invalid { 1282 ui.colored_label( 1283 Color32::RED, 1284 "Fast forward multiplier must be a positive integer", 1285 ); 1286 } 1287 1288 ui.horizontal(|ui| { 1289 ui.add( 1290 NumericTextEdit::new( 1291 &mut self.state.rewind_buffer_len_text, 1292 &mut self.config.common.rewind_buffer_length_seconds, 1293 &mut self.state.rewind_buffer_len_invalid, 1294 ) 1295 .desired_width(30.0), 1296 ); 1297 1298 ui.label("Rewind buffer length in seconds"); 1299 }); 1300 if self.state.rewind_buffer_len_invalid { 1301 ui.colored_label( 1302 Color32::RED, 1303 "Rewind buffer length must be a non-negative integer", 1304 ); 1305 } 1306 1307 ui.checkbox( 1308 &mut self.config.common.load_recent_state_at_launch, 1309 "Load most recent save state at launch", 1310 ); 1311 }); 1312 if !open { 1313 self.state.open_windows.remove(&OpenWindow::Hotkeys); 1314 } 1315 } 1316 1317 fn disable_if_waiting_for_input(&self, ui: &mut Ui) { 1318 if self.state.input_collection.is_some() { 1319 ui.disable(); 1320 } 1321 } 1322 1323 fn render_input_buttons( 1324 &mut self, 1325 id: &str, 1326 mapping: InputMappingSet, 1327 buttons: &[GenericButton], 1328 ui: &mut Ui, 1329 ) { 1330 Grid::new(id).show(ui, |ui| { 1331 for button in buttons { 1332 ui.label(format!("{}:", button.label())); 1333 1334 let Some(current_value) = button.access_value(mapping, &mut self.config.input) 1335 else { 1336 continue; 1337 }; 1338 1339 let current_value_str = format_input_str(current_value.as_ref()); 1340 if ui.button(current_value_str).clicked() { 1341 self.state.input_collection = Some(InputCollectionState::new( 1342 &self.joysticks.borrow(), 1343 vec![*button], 1344 mapping, 1345 false, 1346 )); 1347 } 1348 1349 if ui.button("Clear").clicked() { 1350 *current_value = None; 1351 } 1352 1353 if let Some(turbo_value) = 1354 button.access_value_turbo(mapping, &mut self.config.input) 1355 { 1356 ui.label("Turbo"); 1357 1358 let turbo_value_str = format_input_str(turbo_value.as_ref()); 1359 if ui.button(turbo_value_str).clicked() { 1360 self.state.input_collection = Some(InputCollectionState::new( 1361 &self.joysticks.borrow(), 1362 vec![*button], 1363 mapping, 1364 true, 1365 )); 1366 } 1367 1368 if ui.button("Clear").clicked() { 1369 *turbo_value = None; 1370 } 1371 } 1372 1373 ui.end_row(); 1374 } 1375 }); 1376 } 1377 1378 fn render_mapping_set_selector(&mut self, window: OpenWindow, ui: &mut Ui) -> InputMappingSet { 1379 let field = self.state.input_mapping_sets.entry(window).or_default(); 1380 1381 ui.horizontal(|ui| { 1382 for set in [InputMappingSet::One, InputMappingSet::Two] { 1383 let button = Button::new(set.label()).selected(*field == set); 1384 if ui.add(button).clicked() { 1385 *field = set; 1386 } 1387 } 1388 }); 1389 1390 *field 1391 } 1392 1393 fn render_configure_all_button( 1394 &mut self, 1395 buttons: &[GenericButton], 1396 mapping: InputMappingSet, 1397 ui: &mut Ui, 1398 ) { 1399 if ui.button("Configure all").clicked() { 1400 self.state.input_collection = Some(InputCollectionState::new( 1401 &self.joysticks.borrow(), 1402 buttons.to_vec(), 1403 mapping, 1404 false, 1405 )); 1406 } 1407 } 1408} 1409 1410fn format_input_str(value: Option<&Vec<GenericInput>>) -> String { 1411 let none = || "<None>".into(); 1412 1413 let Some(value) = value else { 1414 return none(); 1415 }; 1416 1417 if value.is_empty() { 1418 return none(); 1419 } 1420 1421 let s: Vec<_> = value.iter().map(|&input| input.to_string()).collect(); 1422 1423 s.join(" + ") 1424} 1425 1426#[derive(Debug, Clone, Copy, PartialEq, Eq)] 1427enum HotkeyCategory { 1428 General, 1429 SaveState, 1430} 1431 1432trait HotkeyExt { 1433 fn category(self) -> HotkeyCategory; 1434} 1435 1436impl HotkeyExt for Hotkey { 1437 fn category(self) -> HotkeyCategory { 1438 use Hotkey::*; 1439 1440 match self { 1441 PowerOff | Exit | ToggleFullscreen | SoftReset | HardReset | Pause | StepFrame 1442 | FastForward | Rewind | CancelMouseInput | ToggleOverclocking | OpenDebugger => { 1443 HotkeyCategory::General 1444 } 1445 SaveState | LoadState | NextSaveStateSlot | PrevSaveStateSlot | SaveStateSlot0 1446 | SaveStateSlot1 | SaveStateSlot2 | SaveStateSlot3 | SaveStateSlot4 1447 | SaveStateSlot5 | SaveStateSlot6 | SaveStateSlot7 | SaveStateSlot8 1448 | SaveStateSlot9 | LoadStateSlot0 | LoadStateSlot1 | LoadStateSlot2 1449 | LoadStateSlot3 | LoadStateSlot4 | LoadStateSlot5 | LoadStateSlot6 1450 | LoadStateSlot7 | LoadStateSlot8 | LoadStateSlot9 => HotkeyCategory::SaveState, 1451 } 1452 } 1453} 1454 1455fn hotkey_vec(category: HotkeyCategory) -> Vec<GenericButton> { 1456 Hotkey::ALL 1457 .into_iter() 1458 .filter_map(|hotkey| { 1459 (hotkey.category() == category).then_some(GenericButton::Hotkey(hotkey)) 1460 }) 1461 .collect() 1462}