1mod helptext; 2 3use crate::app::widgets::{NumericTextEdit, SavePathSelect}; 4use crate::app::{App, OpenWindow, widgets}; 5use egui::{Color32, Context, Grid, Slider, TextEdit, Ui, Window}; 6use jgenesis_native_config::EguiTheme; 7use jgenesis_native_config::common::{CheatPath, HideMouseCursor, PauseEmulator}; 8use jgenesis_native_config::paths::{ConfigDirType, ConfigWithPath}; 9use jgenesis_renderer::config::{ 10 AntiDitherShader, FilterMode, FrameRotation, NtscShaderConfig, PreprocessShader, 11 PrescaleFactor, VSyncMode, WgpuBackend, WgpuPowerPreference, 12}; 13use rfd::FileDialog; 14use std::num::{NonZeroU8, NonZeroU32}; 15 16impl App { 17 pub(super) fn render_common_video_settings(&mut self, ctx: &Context) { 18 const WINDOW: OpenWindow = OpenWindow::CommonVideo; 19 20 let mut open = true; 21 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 22 widgets::render_vertical_scroll_area(ui, |ui| { 23 self.render_fullscreen_settings(ui, WINDOW); 24 self.render_window_size_setting(ui, WINDOW); 25 self.render_borderless_setting(ui, WINDOW); 26 self.render_integer_height_scaling_settings(ui, WINDOW); 27 self.render_wgpu_backend_setting(ui, WINDOW); 28 self.render_wgpu_power_preference_setting(ui, WINDOW); 29 self.render_frame_rotation_setting(ui, WINDOW); 30 }); 31 32 self.render_help_text(ui, WINDOW); 33 }); 34 if !open { 35 self.state.open_windows.remove(&WINDOW); 36 } 37 } 38 39 pub(super) fn render_video_filtering_settings(&mut self, ctx: &Context) { 40 const WINDOW: OpenWindow = OpenWindow::CommonFilter; 41 42 let mut open = true; 43 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 44 widgets::render_vertical_scroll_area(ui, |ui| { 45 self.render_preprocess_shader_setting(ui, WINDOW); 46 self.render_anti_dither_setting(ui, WINDOW); 47 self.render_scanlines_setting(ui, WINDOW); 48 self.render_prescaling_settings(ui, WINDOW); 49 self.render_filter_mode_setting(ui, WINDOW); 50 }); 51 52 self.render_help_text(ui, WINDOW); 53 }); 54 if !open { 55 self.state.open_windows.remove(&WINDOW); 56 } 57 } 58 59 fn render_fullscreen_settings(&mut self, ui: &mut Ui, window: OpenWindow) { 60 let rect = ui 61 .checkbox(&mut self.config.common.launch_in_fullscreen, "Launch in fullscreen") 62 .interact_rect; 63 if ui.rect_contains_pointer(rect) { 64 self.state.help_text.insert(window, helptext::FULLSCREEN); 65 } 66 } 67 68 fn render_window_size_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 69 const MIN_SIZE: NonZeroU8 = NonZeroU8::new(1).unwrap(); 70 const MAX_SIZE: NonZeroU8 = NonZeroU8::new(10).unwrap(); 71 72 let rect = ui 73 .horizontal(|ui| { 74 ui.label("Initial window size:"); 75 76 ui.add( 77 Slider::new(&mut self.config.common.initial_window_size, MIN_SIZE..=MAX_SIZE) 78 .custom_formatter(|n, _| { 79 let n = n as u8; 80 format!("{n}x") 81 }), 82 ); 83 }) 84 .response 85 .interact_rect; 86 if ui.rect_contains_pointer(rect) { 87 self.state.help_text.insert(window, helptext::INITIAL_WINDOW_SIZE); 88 } 89 } 90 91 fn render_borderless_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 92 let rect = ui 93 .checkbox(&mut self.config.common.borderless_window, "Borderless window") 94 .interact_rect; 95 if ui.rect_contains_pointer(rect) { 96 self.state.help_text.insert(window, helptext::BORDERLESS_WINDOW); 97 } 98 } 99 100 fn render_wgpu_backend_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 101 let rect = ui 102 .group(|ui| { 103 ui.add_enabled_ui(!self.emu_runner.status().is_running(), |ui| { 104 ui.label("wgpu backend"); 105 ui.horizontal(|ui| { 106 for (value, label) in [ 107 (WgpuBackend::Auto, "Auto"), 108 (WgpuBackend::Vulkan, "Vulkan"), 109 (WgpuBackend::DirectX12, "DirectX 12"), 110 ] { 111 ui.radio_value(&mut self.config.common.wgpu_backend, value, label); 112 } 113 }); 114 }); 115 }) 116 .response 117 .interact_rect; 118 if ui.rect_contains_pointer(rect) { 119 self.state.help_text.insert(window, helptext::WGPU_BACKEND); 120 } 121 } 122 123 fn render_wgpu_power_preference_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 124 let rect = ui 125 .group(|ui| { 126 ui.add_enabled_ui(!self.emu_runner.status().is_running(), |ui| { 127 ui.label("wgpu power preference"); 128 ui.horizontal(|ui| { 129 for (value, label) in [ 130 (WgpuPowerPreference::HighPerformance, "High performance"), 131 (WgpuPowerPreference::LowPower, "Low power"), 132 (WgpuPowerPreference::None, "No preference"), 133 ] { 134 ui.radio_value( 135 &mut self.config.common.wgpu_power_preference, 136 value, 137 label, 138 ); 139 } 140 }); 141 }); 142 }) 143 .response 144 .interact_rect; 145 if ui.rect_contains_pointer(rect) { 146 self.state.help_text.insert(window, helptext::WGPU_POWER_PREFERENCE); 147 } 148 } 149 150 fn render_filter_mode_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 151 ui.group(|ui| { 152 let rect = ui 153 .scope(|ui| { 154 ui.label("Image filtering"); 155 ui.horizontal(|ui| { 156 ui.radio_value( 157 &mut self.config.common.filter_mode, 158 FilterMode::Nearest, 159 "Nearest neighbor", 160 ); 161 ui.radio_value( 162 &mut self.config.common.filter_mode, 163 FilterMode::Linear, 164 "Linear interpolation", 165 ); 166 }); 167 }) 168 .response 169 .interact_rect; 170 171 if ui.rect_contains_pointer(rect) { 172 self.state.help_text.insert(window, helptext::FILTER_MODE); 173 } 174 175 let rect = ui 176 .checkbox( 177 &mut self.config.common.supersample_minification, 178 "Supersample when minifying", 179 ) 180 .interact_rect; 181 if ui.rect_contains_pointer(rect) { 182 self.state.help_text.insert(window, helptext::SUPERSAMPLE_MINIFICATION); 183 } 184 }); 185 } 186 187 fn render_frame_rotation_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 188 let rect = ui 189 .group(|ui| { 190 ui.label("Frame rotation"); 191 192 ui.horizontal(|ui| { 193 for (value, label) in [ 194 (FrameRotation::None, "None"), 195 (FrameRotation::Clockwise, "90° clockwise"), 196 (FrameRotation::OneEighty, "180°"), 197 (FrameRotation::Counterclockwise, "90° counterclockwise"), 198 ] { 199 ui.radio_value(&mut self.config.common.frame_rotation, value, label); 200 } 201 }); 202 }) 203 .response 204 .interact_rect; 205 if ui.rect_contains_pointer(rect) { 206 self.state.help_text.insert(window, helptext::FRAME_ROTATION); 207 } 208 } 209 210 fn render_preprocess_shader_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 211 let rect = ui 212 .group(|ui| { 213 ui.label("Video shader"); 214 215 ui.radio_value( 216 &mut self.config.common.preprocess_shader, 217 PreprocessShader::None, 218 "None", 219 ); 220 221 let composite_enabled = !self.emu_runner.status().is_running_handheld(); 222 223 ui.horizontal(|ui| { 224 ui.add_enabled_ui(composite_enabled, |ui| { 225 ui.radio_value( 226 &mut self.config.common.preprocess_shader, 227 PreprocessShader::NtscComposite, 228 "NTSC composite", 229 ); 230 }); 231 232 ui.collapsing("NTSC shader settings", |ui| { 233 ui.vertical(|ui| { 234 let fmt_2f = |v: f64, _| format!("{v:.2}"); 235 236 ui.style_mut().spacing.slider_width = 225.0; 237 238 ui.add( 239 Slider::new(&mut self.config.common.ntsc.brightness, 0.0..=5.0) 240 .step_by(0.01) 241 .text("Brightness") 242 .custom_formatter(fmt_2f), 243 ); 244 245 ui.add( 246 Slider::new(&mut self.config.common.ntsc.saturation, 0.0..=5.0) 247 .step_by(0.01) 248 .text("Saturation") 249 .custom_formatter(fmt_2f), 250 ); 251 252 ui.add( 253 Slider::new(&mut self.config.common.ntsc.gamma, 0.1..=5.0) 254 .step_by(0.1) 255 .text("Gamma") 256 .custom_formatter(|v: f64, _| format!("{v:.1}")), 257 ); 258 259 if ui.button("Default").clicked() { 260 self.config.common.ntsc = NtscShaderConfig::default(); 261 } 262 }); 263 }); 264 }); 265 266 ui.horizontal(|ui| { 267 for (shader, label) in [ 268 (PreprocessShader::Xbrz2x, "xBRZ 2x"), 269 (PreprocessShader::Xbrz3x, "xBRZ 3x"), 270 (PreprocessShader::Xbrz4x, "xBRZ 4x"), 271 (PreprocessShader::Xbrz5x, "xBRZ 5x"), 272 (PreprocessShader::Xbrz6x, "xBRZ 6x"), 273 ] { 274 ui.radio_value(&mut self.config.common.preprocess_shader, shader, label); 275 } 276 }); 277 278 ui.horizontal(|ui| { 279 ui.radio_value( 280 &mut self.config.common.preprocess_shader, 281 PreprocessShader::Mmpx, 282 "MMPX", 283 ); 284 ui.radio_value( 285 &mut self.config.common.preprocess_shader, 286 PreprocessShader::MmpxEnhanced, 287 "MMPX Enhanced", 288 ); 289 }); 290 291 ui.horizontal(|ui| { 292 ui.radio_value( 293 &mut self.config.common.preprocess_shader, 294 PreprocessShader::HorizontalBlurTwoPixels, 295 "Horizontal blur (2px)", 296 ); 297 ui.radio_value( 298 &mut self.config.common.preprocess_shader, 299 PreprocessShader::HorizontalBlurThreePixels, 300 "Horizontal blur (3px)", 301 ); 302 ui.radio_value( 303 &mut self.config.common.preprocess_shader, 304 PreprocessShader::HorizontalBlurSnesAdaptive, 305 "Horizontal blur (SNES adaptive)", 306 ) 307 .on_hover_text(concat!( 308 "Always maintains the effect of blurring", 309 " 3px horizontally at 512px horizontal resolution" 310 )); 311 }); 312 }) 313 .response 314 .interact_rect; 315 if ui.rect_contains_pointer(rect) { 316 self.state.help_text.insert(window, helptext::PREPROCESS_SHADER); 317 } 318 } 319 320 fn render_anti_dither_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 321 ui.add_enabled_ui(!self.config.common.preprocess_shader.exclude_anti_dither(), |ui| { 322 let rect = ui 323 .group(|ui| { 324 ui.label("Anti-dither shader"); 325 326 ui.horizontal(|ui| { 327 for (value, label) in [ 328 (AntiDitherShader::None, "None"), 329 (AntiDitherShader::Weak, "Conservative"), 330 (AntiDitherShader::Strong, "Aggressive"), 331 ] { 332 ui.radio_value( 333 &mut self.config.common.anti_dither_shader, 334 value, 335 label, 336 ); 337 } 338 }); 339 }) 340 .response 341 .interact_rect; 342 if ui.rect_contains_pointer(rect) { 343 self.state.help_text.insert(window, helptext::ANTI_DITHER_SHADER); 344 } 345 }); 346 } 347 348 fn render_scanlines_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 349 let rect = ui 350 .group(|ui| { 351 ui.label("Scanlines"); 352 353 ui.horizontal(|ui| { 354 ui.checkbox(&mut self.config.common.scanlines_enabled, "Enabled"); 355 356 ui.add_enabled_ui(self.config.common.scanlines_enabled, |ui| { 357 ui.add( 358 Slider::new(&mut self.config.common.scanlines_brightness, 0.0..=1.0) 359 .text("Brightness") 360 .step_by(0.1) 361 .custom_formatter(|value, _| { 362 format!("{:.0}%", (value * 100.0).round()) 363 }), 364 ); 365 }); 366 }); 367 }) 368 .response 369 .interact_rect; 370 if ui.rect_contains_pointer(rect) { 371 self.state.help_text.insert(window, helptext::SCANLINES); 372 } 373 } 374 375 fn render_prescaling_settings(&mut self, ui: &mut Ui, window: OpenWindow) { 376 let rect = ui 377 .group(|ui| { 378 ui.label("Prescaling"); 379 380 ui.checkbox(&mut self.config.common.auto_prescale, "Auto-prescale enabled"); 381 382 ui.add_enabled_ui(!self.config.common.auto_prescale, |ui| { 383 let mut add_slider = 384 |factor: &mut PrescaleFactor, factor_raw: &mut u32, label: &str| { 385 if ui.add(Slider::new(factor_raw, 1..=16).text(label)).changed() 386 && let Some(prescale_factor) = NonZeroU32::new(*factor_raw) 387 { 388 *factor = prescale_factor.into(); 389 } 390 }; 391 392 add_slider( 393 &mut self.config.common.prescale_width, 394 &mut self.state.prescale_width_raw, 395 "Width scale", 396 ); 397 add_slider( 398 &mut self.config.common.prescale_height, 399 &mut self.state.prescale_height_raw, 400 "Height scale", 401 ); 402 }); 403 }) 404 .response 405 .interact_rect; 406 if ui.rect_contains_pointer(rect) { 407 self.state.help_text.insert(window, helptext::PRESCALING); 408 } 409 } 410 411 fn render_integer_height_scaling_settings(&mut self, ui: &mut Ui, window: OpenWindow) { 412 let rect = ui 413 .checkbox( 414 &mut self.config.common.force_integer_height_scaling, 415 "Force integer height scaling", 416 ) 417 .interact_rect; 418 if ui.rect_contains_pointer(rect) { 419 self.state.help_text.insert(window, helptext::INTEGER_HEIGHT_SCALING); 420 } 421 } 422 423 pub(super) fn render_common_audio_settings(&mut self, ctx: &Context) { 424 const WINDOW: OpenWindow = OpenWindow::CommonAudio; 425 426 const TEXT_EDIT_WIDTH: f32 = 50.0; 427 428 let mut open = true; 429 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 430 let rect = ui.checkbox(&mut self.config.common.mute_audio, "Mute audio").interact_rect; 431 if ui.rect_contains_pointer(rect) { 432 self.state.help_text.insert(WINDOW, helptext::MUTE_AUDIO); 433 } 434 435 ui.add_space(5.0); 436 let rect = ui 437 .group(|ui| { 438 ui.label("Output sample rate"); 439 440 ui.radio_value( 441 &mut self.config.common.audio_output_frequency, 442 48000, 443 "48000 Hz (Recommended)", 444 ); 445 ui.radio_value( 446 &mut self.config.common.audio_output_frequency, 447 44100, 448 "44100 Hz", 449 ); 450 }) 451 .response 452 .interact_rect; 453 if ui.rect_contains_pointer(rect) { 454 self.state.help_text.insert(WINDOW, helptext::AUDIO_SAMPLE_RATE); 455 } 456 457 ui.add_space(10.0); 458 459 let rect = ui 460 .horizontal(|ui| { 461 ui.add( 462 NumericTextEdit::new( 463 &mut self.state.audio_gain_text, 464 &mut self.config.common.audio_gain_db, 465 &mut self.state.audio_gain_invalid, 466 ) 467 .with_validation(f64::is_finite) 468 .desired_width(TEXT_EDIT_WIDTH), 469 ); 470 471 ui.label("Audio gain (dB) (+/-)"); 472 }) 473 .response 474 .interact_rect; 475 if ui.rect_contains_pointer(rect) { 476 self.state.help_text.insert(WINDOW, helptext::AUDIO_GAIN); 477 } 478 479 if self.state.audio_gain_invalid { 480 ui.colored_label(Color32::RED, "Audio gain must be a finite decimal number"); 481 } 482 483 self.render_help_text(ui, WINDOW); 484 }); 485 if !open { 486 self.state.open_windows.remove(&WINDOW); 487 } 488 } 489 490 pub(super) fn render_sync_settings(&mut self, ctx: &Context) { 491 const WINDOW: OpenWindow = OpenWindow::Synchronization; 492 493 const TEXT_EDIT_WIDTH: f32 = 50.0; 494 const MIN_DEVICE_QUEUE_SIZE: u32 = 8; 495 const MIN_AUDIO_SYNC_THRESHOLD: u32 = 8; 496 497 let mut open = true; 498 Window::new(WINDOW.title()).open(&mut open).show(ctx, |ui| { 499 let rect = ui 500 .group(|ui| { 501 ui.label("VSync mode"); 502 503 ui.horizontal(|ui| { 504 ui.radio_value( 505 &mut self.config.common.vsync_mode, 506 VSyncMode::Enabled, 507 "Enabled", 508 ); 509 ui.radio_value( 510 &mut self.config.common.vsync_mode, 511 VSyncMode::Disabled, 512 "Disabled", 513 ); 514 ui.radio_value(&mut self.config.common.vsync_mode, VSyncMode::Fast, "Fast"); 515 }); 516 }) 517 .response 518 .interact_rect; 519 if ui.rect_contains_pointer(rect) { 520 self.state.help_text.insert(WINDOW, helptext::VSYNC_MODE); 521 } 522 523 ui.add_space(10.0); 524 525 let rect = ui 526 .checkbox(&mut self.config.common.frame_time_sync, "Frame time sync enabled") 527 .interact_rect; 528 if ui.rect_contains_pointer(rect) { 529 self.state.help_text.insert(WINDOW, helptext::FRAME_TIME_SYNC); 530 } 531 532 let rect = 533 ui.checkbox(&mut self.config.common.audio_sync, "Audio sync enabled").interact_rect; 534 if ui.rect_contains_pointer(rect) { 535 self.state.help_text.insert(WINDOW, helptext::AUDIO_SYNC); 536 } 537 538 let rect = ui 539 .checkbox( 540 &mut self.config.common.audio_dynamic_resampling_ratio, 541 "Audio dynamic resampling ratio enabled", 542 ) 543 .interact_rect; 544 if ui.rect_contains_pointer(rect) { 545 self.state.help_text.insert(WINDOW, helptext::AUDIO_DYNAMIC_RESAMPLING); 546 } 547 548 let any_sync_enabled = self.config.common.vsync_mode == VSyncMode::Enabled 549 || self.config.common.audio_sync 550 || self.config.common.frame_time_sync; 551 if !any_sync_enabled { 552 ui.add_space(5.0); 553 ui.colored_label( 554 Color32::RED, 555 "No sync enabled; emulator will run at uncapped speed", 556 ); 557 } 558 559 ui.add_space(10.0); 560 561 let rect = ui 562 .horizontal(|ui| { 563 ui.add( 564 NumericTextEdit::new( 565 &mut self.state.audio_buffer_size_text, 566 &mut self.config.common.audio_buffer_size, 567 &mut self.state.audio_buffer_size_invalid, 568 ) 569 .with_validation(|value| value >= MIN_AUDIO_SYNC_THRESHOLD) 570 .desired_width(TEXT_EDIT_WIDTH), 571 ); 572 573 ui.label("Audio buffer size (samples)"); 574 }) 575 .response 576 .interact_rect; 577 if ui.rect_contains_pointer(rect) { 578 self.state.help_text.insert(WINDOW, helptext::AUDIO_BUFFER_SIZE); 579 } 580 581 if self.state.audio_buffer_size_invalid { 582 ui.colored_label( 583 Color32::RED, 584 format!("Audio sync threshold must be at least {MIN_AUDIO_SYNC_THRESHOLD}"), 585 ); 586 } 587 588 let rect = ui 589 .horizontal(|ui| { 590 ui.add( 591 NumericTextEdit::new( 592 &mut self.state.audio_hardware_queue_size_text, 593 &mut self.config.common.audio_hardware_queue_size, 594 &mut self.state.audio_hardware_queue_size_invalid, 595 ) 596 .with_validation(|value| value >= MIN_DEVICE_QUEUE_SIZE) 597 .desired_width(TEXT_EDIT_WIDTH), 598 ); 599 600 ui.label("Audio hardware queue size (samples)"); 601 }) 602 .response 603 .interact_rect; 604 if ui.rect_contains_pointer(rect) { 605 self.state.help_text.insert(WINDOW, helptext::AUDIO_HARDWARE_QUEUE_SIZE); 606 } 607 608 if self.state.audio_hardware_queue_size_invalid { 609 ui.colored_label( 610 Color32::RED, 611 format!("Audio device queue size must be at least {MIN_DEVICE_QUEUE_SIZE}"), 612 ); 613 } 614 615 ui.add_space(5.0); 616 617 let estimated_audio_latency_ms = self.estimate_audio_latency_ms(); 618 ui.label(format!("Estimated audio latency: {estimated_audio_latency_ms} ms")); 619 620 self.render_help_text(ui, WINDOW); 621 }); 622 if !open { 623 self.state.open_windows.remove(&WINDOW); 624 } 625 } 626 627 fn estimate_audio_latency_ms(&self) -> u32 { 628 let audio_buffer_size: f64 = self.config.common.audio_buffer_size.into(); 629 let audio_hardware_queue_size: f64 = self.config.common.audio_hardware_queue_size.into(); 630 let latency_secs = (audio_buffer_size + audio_hardware_queue_size) 631 / (self.config.common.audio_output_frequency as f64); 632 633 (latency_secs * 1000.0).round() as u32 634 } 635 636 pub(super) fn render_path_settings(&mut self, ctx: &Context) { 637 let mut open = true; 638 Window::new(OpenWindow::Paths.title()).open(&mut open).default_width(500.0).show( 639 ctx, 640 |ui| { 641 widgets::render_vertical_scroll_area(ui, |ui| { 642 let using_override = 643 matches!(self.config_dir_type, ConfigDirType::Override { .. }); 644 ui.add_enabled_ui(!using_override, |ui| { 645 let prev_config_dir_type = self.config_dir_type.clone(); 646 647 ui.group(|ui| { 648 ui.label("Settings path"); 649 650 ui.add_enabled_ui(self.config_dirs.user_profile_dir.is_some(), |ui| { 651 ui.radio_value( 652 &mut self.config_dir_type, 653 ConfigDirType::UserProfile, 654 "User profile directory", 655 ); 656 }); 657 658 ui.add_enabled_ui(self.config_dirs.emulator_dir.is_some(), |ui| { 659 ui.radio_value( 660 &mut self.config_dir_type, 661 ConfigDirType::EmulatorDirectory, 662 "Emulator directory (Portable)", 663 ); 664 }); 665 666 ui.label(format!(" {}", self.config_path.display())); 667 }); 668 669 if self.config_dir_type != prev_config_dir_type { 670 self.handle_config_dir_type_change(ctx); 671 } 672 }); 673 674 ui.add(SavePathSelect::new( 675 "Game save file path", 676 &mut self.config.common.save_path, 677 &mut self.config.common.custom_save_path, 678 )); 679 680 ui.add(SavePathSelect::new( 681 "Save state path", 682 &mut self.config.common.state_path, 683 &mut self.config.common.custom_state_path, 684 )); 685 686 ui.group(|ui| { 687 ui.label("Cheats path"); 688 689 ui.horizontal(|ui| { 690 ui.radio_value( 691 &mut self.config.common.cheats_path, 692 CheatPath::SettingsFolder, 693 "Same folder as main settings", 694 ); 695 ui.radio_value( 696 &mut self.config.common.cheats_path, 697 CheatPath::EmulatorFolder, 698 "Emulator folder", 699 ); 700 ui.radio_value( 701 &mut self.config.common.cheats_path, 702 CheatPath::Custom, 703 "Custom", 704 ); 705 }); 706 707 ui.add_enabled_ui( 708 self.config.common.cheats_path == CheatPath::Custom, 709 |ui| { 710 widgets::render_custom_path_select( 711 ui, 712 &mut self.config.common.cheats_custom_path, 713 ); 714 }, 715 ); 716 }); 717 718 ui.add_space(10.0); 719 720 ui.group(|ui| { 721 ui.heading("ROM search directories"); 722 723 ui.add_space(5.0); 724 725 let mut changed = false; 726 let mut to_remove: Option<usize> = None; 727 728 Grid::new("rom_search_dirs").show(ui, |ui| { 729 ui.heading("Path"); 730 ui.heading(""); 731 ui.heading("Recursive"); 732 ui.heading(""); 733 ui.end_row(); 734 735 for (i, rom_search_dir) in 736 self.config.rom_search_dirs.iter_mut().enumerate() 737 { 738 match rom_search_dir.path.to_str() { 739 Some(path_str) => { 740 let mut path_str = path_str.to_string(); 741 if ui 742 .add( 743 TextEdit::singleline(&mut path_str) 744 .clip_text(false), 745 ) 746 .changed() 747 { 748 rom_search_dir.path = path_str.into(); 749 changed = true; 750 } 751 } 752 None => { 753 ui.label(rom_search_dir.path.display().to_string()); 754 } 755 } 756 757 if ui.button("Browse...").clicked() { 758 let mut file_dialog = FileDialog::new(); 759 if let Some(current_parent) = rom_search_dir.path.parent() { 760 file_dialog = file_dialog.set_directory(current_parent); 761 } 762 763 if let Some(path) = file_dialog.pick_folder() { 764 rom_search_dir.path = path; 765 changed = true; 766 } 767 } 768 769 ui.centered_and_justified(|ui| { 770 changed |= 771 ui.checkbox(&mut rom_search_dir.recursive, "").changed(); 772 }); 773 774 if ui.button("Remove").clicked() { 775 to_remove = Some(i); 776 } 777 778 ui.end_row(); 779 } 780 }); 781 782 if let Some(to_remove) = to_remove 783 && to_remove < self.config.rom_search_dirs.len() 784 { 785 self.config.rom_search_dirs.remove(to_remove); 786 changed = true; 787 } 788 789 if changed { 790 self.request_rom_list_scan(); 791 } 792 793 if ui.button("Add").clicked() { 794 self.add_rom_search_directory(); 795 } 796 }); 797 }); 798 }, 799 ); 800 if !open { 801 self.state.open_windows.remove(&OpenWindow::Paths); 802 } 803 } 804 805 fn handle_config_dir_type_change(&mut self, ctx: &Context) { 806 match &self.config_dir_type { 807 ConfigDirType::EmulatorDirectory => { 808 if let Err(err) = self.config_dirs.create_portable_txt() { 809 log::error!("Error creating portable.txt file: {err}"); 810 } 811 } 812 _ => { 813 if let Err(err) = self.config_dirs.delete_portable_txt() { 814 log::error!("Error deleting portable.txt file: {err}"); 815 } 816 } 817 } 818 819 let prev_rom_search_dirs = self.config.rom_search_dirs.clone(); 820 let prev_list_filters = self.config.list_filters.clone(); 821 822 // Try to load config from new path, but keep config unchanged if unable to load (e.g. file does not exist) 823 let new_config = ConfigWithPath::load_from_dir_or_default( 824 &self.config_dirs, 825 &self.config_dir_type, 826 || self.config.clone(), 827 ); 828 829 log::info!("Config path changed to '{}'", new_config.path.display()); 830 831 if !new_config.path.exists() { 832 log::info!( 833 "Saving current settings to new config path '{}'", 834 new_config.path.display() 835 ); 836 if let Err(err) = new_config.save_config() { 837 log::error!( 838 "Error saving current settings to '{}': {err}", 839 new_config.path.display() 840 ); 841 } 842 } 843 844 self.config = new_config.config; 845 self.config_path = new_config.path; 846 847 self.state.update_config_derived_fields(&self.config, ctx); 848 849 if prev_rom_search_dirs != self.config.rom_search_dirs 850 || prev_list_filters != self.config.list_filters 851 { 852 self.request_rom_list_scan(); 853 } 854 } 855 856 pub(super) fn render_interface_settings(&mut self, ctx: &Context) { 857 let mut open = true; 858 Window::new(OpenWindow::Interface.title()).open(&mut open).resizable(false).show( 859 ctx, 860 |ui| { 861 ui.group(|ui| { 862 ui.label("Pause emulator automatically"); 863 864 for (option, label) in [ 865 (PauseEmulator::Never, "Never"), 866 ( 867 PauseEmulator::EmulatorLosesFocus, 868 "When emulator window is in background", 869 ), 870 ( 871 PauseEmulator::ApplicationLosesFocus, 872 "When entire application is in background", 873 ), 874 ] { 875 ui.radio_value(&mut self.config.common.pause_emulator, option, label); 876 } 877 }); 878 879 ui.add_space(5.0); 880 881 ui.group(|ui| { 882 ui.label("Hide mouse cursor over emulator window"); 883 884 ui.radio_value( 885 &mut self.config.common.hide_mouse_cursor, 886 HideMouseCursor::Fullscreen, 887 "Only when fullscreen", 888 ); 889 ui.radio_value( 890 &mut self.config.common.hide_mouse_cursor, 891 HideMouseCursor::Always, 892 "Always", 893 ); 894 ui.radio_value( 895 &mut self.config.common.hide_mouse_cursor, 896 HideMouseCursor::Never, 897 "Never", 898 ); 899 }); 900 901 ui.add_space(5.0); 902 903 ui.group(|ui| { 904 ui.label("UI theme"); 905 906 ui.horizontal(|ui| { 907 ui.radio_value( 908 &mut self.config.egui_theme, 909 EguiTheme::SystemDefault, 910 "System default", 911 ); 912 ui.radio_value(&mut self.config.egui_theme, EguiTheme::Dark, "Dark"); 913 ui.radio_value(&mut self.config.egui_theme, EguiTheme::Light, "Light"); 914 }); 915 }); 916 }, 917 ); 918 if !open { 919 self.state.open_windows.remove(&OpenWindow::Interface); 920 } 921 } 922}