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}