1use crate::app::RESERVED_HELP_TEXT_HEIGHT;
2use egui::scroll_area::ScrollAreaOutput;
3use egui::style::ScrollStyle;
4use egui::{Context, Response, ScrollArea, Slider, TextEdit, Ui, Widget, WidgetText, Window};
5use jgenesis_native_config::common::ConfigSavePath;
6use jgenesis_native_driver::extensions::Console;
7use rfd::FileDialog;
8use std::ops::RangeInclusive;
9use std::path::PathBuf;
10use std::str::FromStr;
11
12pub struct NumericTextEdit<'a, T> {
13    text: &'a mut String,
14    value: &'a mut T,
15    invalid: &'a mut bool,
16    validation_fn: Box<dyn Fn(T) -> bool>,
17    desired_width: Option<f32>,
18}
19
20impl<'a, T> NumericTextEdit<'a, T> {
21    pub fn new(text: &'a mut String, value: &'a mut T, invalid: &'a mut bool) -> Self {
22        Self { text, value, invalid, validation_fn: Box::new(|_| true), desired_width: None }
23    }
24
25    pub fn with_validation(mut self, validation_fn: impl Fn(T) -> bool + 'static) -> Self {
26        self.validation_fn = Box::new(validation_fn);
27        self
28    }
29
30    pub fn desired_width(mut self, desired_width: f32) -> Self {
31        self.desired_width = Some(desired_width);
32        self
33    }
34}
35
36impl<T: Copy + FromStr> Widget for NumericTextEdit<'_, T> {
37    fn ui(self, ui: &mut Ui) -> Response {
38        let mut text_edit = TextEdit::singleline(self.text);
39        if let Some(desired_width) = self.desired_width {
40            text_edit = text_edit.desired_width(desired_width);
41        }
42
43        let response = text_edit.ui(ui);
44        if response.changed() {
45            match self.text.parse::<T>() {
46                Ok(value) if (self.validation_fn)(value) => {
47                    *self.value = value;
48                    *self.invalid = false;
49                }
50                _ => {
51                    *self.invalid = true;
52                }
53            }
54        }
55
56        response
57    }
58}
59
60pub struct OptionalPathSelector<'a> {
61    label: &'static str,
62    path: &'a mut Option<PathBuf>,
63    pick_bios_path: fn() -> Option<PathBuf>,
64}
65
66impl<'a> OptionalPathSelector<'a> {
67    pub fn new(
68        label: &'static str,
69        path: &'a mut Option<PathBuf>,
70        pick_bios_path: fn() -> Option<PathBuf>,
71    ) -> Self {
72        Self { label, path, pick_bios_path }
73    }
74}
75
76impl Widget for OptionalPathSelector<'_> {
77    fn ui(self, ui: &mut Ui) -> Response {
78        ui.horizontal(|ui| {
79            ui.label(self.label);
80
81            let button_label = match self.path {
82                Some(path) => path.to_string_lossy(),
83                None => "<None>".into(),
84            };
85            if ui.button(button_label).clicked()
86                && let Some(path) = (self.pick_bios_path)()
87            {
88                *self.path = Some(path);
89            }
90        })
91        .response
92    }
93}
94
95pub fn render_vertical_scroll_area<R>(
96    ui: &mut Ui,
97    add_contents: impl FnOnce(&mut Ui) -> R,
98) -> ScrollAreaOutput<R> {
99    let screen_height = ui.input(|i| i.content_rect().height());
100
101    let mut scroll_area = ScrollArea::vertical().auto_shrink([false, true]);
102
103    let max_scroll_height = screen_height - RESERVED_HELP_TEXT_HEIGHT - 75.0;
104    if max_scroll_height >= 100.0 {
105        scroll_area = scroll_area.max_height(max_scroll_height);
106    }
107
108    ui.scope(|ui| {
109        ui.style_mut().spacing.scroll = ScrollStyle::solid();
110        scroll_area.show(ui, add_contents)
111    })
112    .inner
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum RenderErrorEffect {
117    None,
118    LaunchEmulator(Console),
119}
120
121pub struct BiosErrorStrings<S1: Into<WidgetText>, S2: Into<WidgetText>, S3: Into<WidgetText>> {
122    pub title: S1,
123    pub text: S2,
124    pub button_label: S3,
125}
126
127pub fn render_bios_error<S1, S2, S3>(
128    ctx: &Context,
129    open: &mut bool,
130    BiosErrorStrings { title, text, button_label }: BiosErrorStrings<S1, S2, S3>,
131    path: &mut Option<PathBuf>,
132    console: Console,
133    pick_path: fn() -> Option<PathBuf>,
134) -> RenderErrorEffect
135where
136    S1: Into<WidgetText>,
137    S2: Into<WidgetText>,
138    S3: Into<WidgetText>,
139{
140    let mut path_configured = false;
141    Window::new(title).open(open).resizable(false).show(ctx, |ui| {
142        ui.label(text);
143
144        ui.add_space(10.0);
145
146        ui.horizontal(|ui| {
147            ui.label("Configure now:");
148            if ui.button(button_label).clicked()
149                && let Some(bios_path) = pick_path()
150            {
151                *path = Some(bios_path);
152                path_configured = true;
153            }
154        });
155    });
156
157    if path_configured {
158        *open = false;
159        RenderErrorEffect::LaunchEmulator(console)
160    } else {
161        RenderErrorEffect::None
162    }
163}
164
165pub fn render_custom_path_select(ui: &mut Ui, custom_path: &mut PathBuf) {
166    ui.horizontal(|ui| {
167        ui.label("Custom path:");
168
169        let button_label = custom_path.to_string_lossy();
170        if ui.button(button_label).clicked()
171            && let Some(path) = FileDialog::new().pick_folder()
172        {
173            *custom_path = path;
174        }
175    });
176}
177
178pub struct SavePathSelect<'a> {
179    label: &'a str,
180    save_path: &'a mut ConfigSavePath,
181    custom_path: &'a mut PathBuf,
182}
183
184impl<'a> SavePathSelect<'a> {
185    pub fn new(
186        label: &'a str,
187        save_path: &'a mut ConfigSavePath,
188        custom_path: &'a mut PathBuf,
189    ) -> Self {
190        Self { label, save_path, custom_path }
191    }
192}
193
194impl Widget for SavePathSelect<'_> {
195    fn ui(self, ui: &mut Ui) -> Response {
196        ui.group(|ui| {
197            ui.label(self.label);
198
199            ui.horizontal(|ui| {
200                ui.radio_value(self.save_path, ConfigSavePath::RomFolder, "Same folder as ROM");
201                ui.radio_value(self.save_path, ConfigSavePath::EmulatorFolder, "Emulator folder");
202                ui.radio_value(self.save_path, ConfigSavePath::Custom, "Custom");
203            });
204
205            ui.add_enabled_ui(*self.save_path == ConfigSavePath::Custom, |ui| {
206                render_custom_path_select(ui, self.custom_path);
207            });
208        })
209        .response
210    }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum ClockModifier {
215    Divider,
216    Multiplier,
217}
218
219pub struct OverclockSlider<'a, Num> {
220    pub label: &'a str,
221    pub current_value: &'a mut Num,
222    pub range: RangeInclusive<Num>,
223    pub master_clock: f64,
224    pub default_divider: f64,
225    pub modifier: ClockModifier,
226}
227
228impl<Num: emath::Numeric> Widget for OverclockSlider<'_, Num> {
229    fn ui(self, ui: &mut Ui) -> Response {
230        ui.group(|ui| {
231            ui.label(self.label);
232
233            ui.add(Slider::new(self.current_value, self.range));
234
235            let current_divider = self.current_value.to_f64();
236
237            let (effective_speed_ratio, effective_speed_mhz) = match self.modifier {
238                ClockModifier::Divider => {
239                    let effective_speed_ratio = 100.0 * self.default_divider / current_divider;
240                    let effective_speed_mhz = self.master_clock / current_divider / 1_000_000.0;
241                    (effective_speed_ratio, effective_speed_mhz)
242                }
243                ClockModifier::Multiplier => {
244                    let effective_speed_ratio = 100.0 * current_divider / self.default_divider;
245                    let effective_speed_mhz = self.master_clock * current_divider / 1_000_000.0;
246                    (effective_speed_ratio, effective_speed_mhz)
247                }
248            };
249
250            ui.label(format!(
251                "Effective speed: {effective_speed_mhz:.2} MHz ({}%)",
252                effective_speed_ratio.round()
253            ));
254        })
255        .response
256    }
257}