common.rsannotatedcommon.rssource922 lines · 35.8 KB · raw
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}