From 7bf0a8277ac9d0578d859cce264e670ef322229d Mon Sep 17 00:00:00 2001 From: Alex Janka Date: Fri, 3 Nov 2023 14:31:32 +1100 Subject: [PATCH] mac preferences is all there! --- Cargo.lock | 2 +- gb-emu/src/bin/macos/mod.rs | 1 - gb-emu/src/bin/macos/preferences/views.rs | 258 ++++++++++++++++-- .../bin/macos/preferences/views/widgets.rs | 201 +++++++++++++- lib/src/config/mod.rs | 11 +- 5 files changed, 443 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0aaa6c..02ad7f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,7 +452,7 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cacao" version = "0.4.0-beta2" -source = "git+https://github.com/italicsjenga/cacao#6a3ed259b28f9bfd093293f61cd659247aa7375f" +source = "git+https://github.com/italicsjenga/cacao#6144e9f244abfd15687de00b6cfda6b4606f351a" dependencies = [ "bitmask-enum", "block2", diff --git a/gb-emu/src/bin/macos/mod.rs b/gb-emu/src/bin/macos/mod.rs index 870014e..0e71967 100644 --- a/gb-emu/src/bin/macos/mod.rs +++ b/gb-emu/src/bin/macos/mod.rs @@ -51,7 +51,6 @@ impl AppDelegate for TwincUiApp { fn did_finish_launching(&self) { App::set_menu(menu()); App::activate(); - self.preferences.read().unwrap().show(); } } diff --git a/gb-emu/src/bin/macos/preferences/views.rs b/gb-emu/src/bin/macos/preferences/views.rs index e547960..0164c01 100644 --- a/gb-emu/src/bin/macos/preferences/views.rs +++ b/gb-emu/src/bin/macos/preferences/views.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use cacao::{ layout::Layout, view::{View, ViewDelegate}, @@ -7,10 +9,23 @@ use gb_emu_lib::config::{Config, ConfigManager, ResolutionOverride}; use crate::macos::dispatch; -use self::widgets::{PathView, StepperView, ToggleView}; +use self::widgets::{PathView, StepperView, StepperViewToggle, ToggleView}; mod widgets; +fn make_relative_path(path: PathBuf, base_dir: PathBuf) -> String { + let path = path.canonicalize().unwrap_or(path); + let base_dir = base_dir.canonicalize().unwrap_or(base_dir); + if path.starts_with(&base_dir) { + path.strip_prefix(base_dir).unwrap_or(&path) + } else { + &path + } + .to_str() + .unwrap() + .to_string() +} + pub(crate) struct CorePreferencesContentView { config: Config, config_manager: ConfigManager, @@ -20,10 +35,10 @@ pub(crate) struct CorePreferencesContentView { prefer_cgb: ToggleView, dmg_shader: PathView, dmg_resizable: ToggleView, - dmg_resolution: StepperView, + dmg_resolution: StepperViewToggle, cgb_shader: PathView, cgb_resizable: ToggleView, - cgb_resolution: StepperView, + cgb_resolution: StepperViewToggle, } impl CorePreferencesContentView { @@ -51,14 +66,16 @@ impl CorePreferencesContentView { } CorePreferencesUpdates::PreferCGB => self.config.prefer_cgb = !self.config.prefer_cgb, CorePreferencesUpdates::DmgResolution => { - let val = self.dmg_resolution.update(); - self.config.vulkan_config.dmg_resolution_override = - ResolutionOverride::Scale(val.round() as usize) + if let Some(val) = self.dmg_resolution.update() { + self.config.vulkan_config.dmg_resolution_override = + ResolutionOverride::Scale(val.round() as usize) + } } CorePreferencesUpdates::CgbResolution => { - let val = self.cgb_resolution.update(); - self.config.vulkan_config.cgb_resolution_override = - ResolutionOverride::Scale(val.round() as usize) + if let Some(val) = self.cgb_resolution.update() { + self.config.vulkan_config.cgb_resolution_override = + ResolutionOverride::Scale(val.round() as usize) + } } CorePreferencesUpdates::DmgResizable => { self.config.vulkan_config.dmg_shader_resizable = @@ -68,6 +85,44 @@ impl CorePreferencesContentView { self.config.vulkan_config.cgb_shader_resizable = !self.config.vulkan_config.cgb_shader_resizable } + CorePreferencesUpdates::DmgResolutionEnabled => { + self.dmg_resolution.flip(); + self.config.vulkan_config.dmg_resolution_override = self + .dmg_resolution + .update() + .map(|v| v.round() as usize) + .into(); + } + CorePreferencesUpdates::CgbResolutionEnabled => { + self.cgb_resolution.flip(); + self.config.vulkan_config.cgb_resolution_override = self + .cgb_resolution + .update() + .map(|v| v.round() as usize) + .into(); + } + CorePreferencesUpdates::DmgBootrom(path) => { + self.config.dmg_bootrom = + path.map(|v| make_relative_path(v, self.config_manager.dir())); + self.dmg_bootrom.update(self.config.dmg_bootrom.clone()); + } + CorePreferencesUpdates::CgbBootrom(path) => { + self.config.cgb_bootrom = + path.map(|v| make_relative_path(v, self.config_manager.dir())); + self.cgb_bootrom.update(self.config.cgb_bootrom.clone()); + } + CorePreferencesUpdates::DmgShader(path) => { + self.config.vulkan_config.dmg_shader_path = + path.map(|v| make_relative_path(v, self.config_manager.dir())); + self.dmg_shader + .update(self.config.vulkan_config.dmg_shader_path.clone()); + } + CorePreferencesUpdates::CgbShader(path) => { + self.config.vulkan_config.cgb_shader_path = + path.map(|v| make_relative_path(v, self.config_manager.dir())); + self.cgb_shader + .update(self.config.vulkan_config.cgb_shader_path.clone()); + } } self.config_manager @@ -80,9 +135,15 @@ pub(crate) enum CorePreferencesUpdates { ShowBootrom, PreferCGB, DmgResolution, + DmgResolutionEnabled, CgbResolution, + CgbResolutionEnabled, DmgResizable, CgbResizable, + DmgBootrom(Option), + CgbBootrom(Option), + DmgShader(Option), + CgbShader(Option), } impl ViewDelegate for CorePreferencesContentView { @@ -90,11 +151,19 @@ impl ViewDelegate for CorePreferencesContentView { fn did_load(&mut self, view: View) { self.dmg_bootrom - .configure("DMG bootrom:", "", self.config.dmg_bootrom.clone()); + .configure("DMG bootrom", "", self.config.dmg_bootrom.clone(), |v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::DmgBootrom(v)), + )) + }); view.add_subview(&self.dmg_bootrom.view); self.cgb_bootrom - .configure("CGB bootrom:", "", self.config.cgb_bootrom.clone()); + .configure("CGB bootrom", "", self.config.cgb_bootrom.clone(), |v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::CgbBootrom(v)), + )) + }); view.add_subview(&self.cgb_bootrom.view); self.show_bootrom @@ -121,18 +190,27 @@ impl ViewDelegate for CorePreferencesContentView { ResolutionOverride::Default => 4, }; + self.dmg_resolution.set_suffix(String::from("x")); self.dmg_resolution.configure( - "DMG resolution scale", + "DMG scale override", 1., 10., 1.0, 0, Some(dmg_resolution_override as f64), + self.config.vulkan_config.dmg_resolution_override != ResolutionOverride::Default, |_v| { dispatch(crate::macos::AppMessage::Preferences( super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::DmgResolution), )); }, + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateCore( + CorePreferencesUpdates::DmgResolutionEnabled, + ), + )); + }, ); view.add_subview(&self.dmg_resolution.view); @@ -140,6 +218,11 @@ impl ViewDelegate for CorePreferencesContentView { "DMG shader", "", self.config.vulkan_config.dmg_shader_path.clone(), + |v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::DmgShader(v)), + )) + }, ); view.add_subview(&self.dmg_shader.view); @@ -159,18 +242,27 @@ impl ViewDelegate for CorePreferencesContentView { ResolutionOverride::Default => 4, }; + self.cgb_resolution.set_suffix(String::from("x")); self.cgb_resolution.configure( - "CGB resolution scale", + "CGB scale override", 1., 10., 1.0, 0, Some(cgb_resolution_override as f64), + self.config.vulkan_config.cgb_resolution_override != ResolutionOverride::Default, |_v| { dispatch(crate::macos::AppMessage::Preferences( super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::CgbResolution), )); }, + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateCore( + CorePreferencesUpdates::CgbResolutionEnabled, + ), + )); + }, ); view.add_subview(&self.cgb_resolution.view); @@ -178,6 +270,11 @@ impl ViewDelegate for CorePreferencesContentView { "CGB shader", "", self.config.vulkan_config.cgb_shader_path.clone(), + |v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::CgbShader(v)), + )) + }, ); view.add_subview(&self.cgb_shader.view); @@ -213,7 +310,10 @@ impl ViewDelegate for CorePreferencesContentView { pub(crate) struct StandalonePreferencesContentView { config: StandaloneConfig, config_manager: ConfigManager, + scale_factor: StepperView, group_screenshots_by_rom: ToggleView, + buffers_per_frame: StepperView, + output_buffer_size: StepperView, } impl StandalonePreferencesContentView { @@ -222,6 +322,9 @@ impl StandalonePreferencesContentView { config: config_manager.load_or_create_config(), config_manager, group_screenshots_by_rom: Default::default(), + scale_factor: Default::default(), + buffers_per_frame: Default::default(), + output_buffer_size: Default::default(), } } @@ -230,6 +333,18 @@ impl StandalonePreferencesContentView { StandalonePreferencesUpdates::GroupScreenshotsByRom => { self.config.group_screenshots_by_rom = !self.config.group_screenshots_by_rom } + StandalonePreferencesUpdates::ScaleFactor => { + self.config.scale_factor = self.scale_factor.update().round() as usize + } + StandalonePreferencesUpdates::BuffersPerFrame => { + self.config.buffers_per_frame = self.buffers_per_frame.update().round() as usize + } + StandalonePreferencesUpdates::OutputBufferSize => { + self.config.output_buffer_size = self.output_buffer_size.update_map(|v| { + let i = v.round() as u32; + 2_u32.pow(i) + }); + } } self.config_manager .save_custom_config(self.config.clone()) @@ -239,12 +354,32 @@ impl StandalonePreferencesContentView { pub(crate) enum StandalonePreferencesUpdates { GroupScreenshotsByRom, + ScaleFactor, + BuffersPerFrame, + OutputBufferSize, } impl ViewDelegate for StandalonePreferencesContentView { const NAME: &'static str = "StandalonePreferencesContentView"; fn did_load(&mut self, view: View) { + self.scale_factor.configure( + "Scale factor", + 1., + 16., + 1., + 0, + Some(self.config.scale_factor as f64), + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateStandalone( + StandalonePreferencesUpdates::ScaleFactor, + ), + )) + }, + ); + view.add_subview(&self.scale_factor.view); + self.group_screenshots_by_rom.configure( "Group screenshots by ROM", self.config.group_screenshots_by_rom, @@ -256,16 +391,63 @@ impl ViewDelegate for StandalonePreferencesContentView { )) }, ); - view.add_subview(&self.group_screenshots_by_rom.view); - widgets::auto_layout(&view, vec![&self.group_screenshots_by_rom.view]); + self.buffers_per_frame.configure( + "Buffers per frame", + 1., + 10., + 1., + 0, + Some(self.config.buffers_per_frame as f64), + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateStandalone( + StandalonePreferencesUpdates::BuffersPerFrame, + ), + )) + }, + ); + view.add_subview(&self.buffers_per_frame.view); + + self.output_buffer_size.configure( + "Output buffer size", + 1., + 100., + 1., + 0, + Some((self.config.output_buffer_size as f64).log2()), + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateStandalone( + StandalonePreferencesUpdates::OutputBufferSize, + ), + )) + }, + ); + self.output_buffer_size.update_map(|v| { + let i = v.round() as u32; + 2_u32.pow(i) + }); + view.add_subview(&self.output_buffer_size.view); + + widgets::auto_layout( + &view, + vec![ + &self.scale_factor.view, + &self.group_screenshots_by_rom.view, + &self.buffers_per_frame.view, + &self.output_buffer_size.view, + ], + ); } } pub(crate) struct VstPreferencesContentView { config: twinc_emu_vst::VstConfig, config_manager: ConfigManager, + scale_factor: StepperView, + rom: PathView, force_skip_bootrom: ToggleView, } @@ -275,6 +457,8 @@ impl VstPreferencesContentView { config: config_manager.load_or_create_config(), config_manager, force_skip_bootrom: Default::default(), + scale_factor: Default::default(), + rom: Default::default(), } } @@ -283,6 +467,15 @@ impl VstPreferencesContentView { VstPreferencesUpdates::ForceSkipBootrom => { self.config.force_skip_bootrom = !self.config.force_skip_bootrom } + VstPreferencesUpdates::ScaleFactor => { + self.config.scale_factor = self.scale_factor.update().round() as usize + } + VstPreferencesUpdates::Rom(path) => { + if let Some(path) = path { + self.config.rom = make_relative_path(path, self.config_manager.dir()); + } + self.rom.update(Some(self.config.rom.clone())); + } } self.config_manager .save_custom_config(self.config.clone()) @@ -292,12 +485,37 @@ impl VstPreferencesContentView { pub(crate) enum VstPreferencesUpdates { ForceSkipBootrom, + ScaleFactor, + Rom(Option), } impl ViewDelegate for VstPreferencesContentView { const NAME: &'static str = "VstPreferencesContentView"; fn did_load(&mut self, view: View) { + self.scale_factor.configure( + "Scale factor", + 1., + 16., + 1., + 0, + Some(self.config.scale_factor as f64), + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateVst(VstPreferencesUpdates::ScaleFactor), + )) + }, + ); + view.add_subview(&self.scale_factor.view); + + self.rom + .configure("ROM", "", Some(self.config.rom.clone()), |path| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateVst(VstPreferencesUpdates::Rom(path)), + )) + }); + view.add_subview(&self.rom.view); + self.force_skip_bootrom.configure( "Force skip bootrom", self.config.force_skip_bootrom, @@ -307,9 +525,15 @@ impl ViewDelegate for VstPreferencesContentView { )) }, ); - view.add_subview(&self.force_skip_bootrom.view); - widgets::auto_layout(&view, vec![&self.force_skip_bootrom.view]); + widgets::auto_layout( + &view, + vec![ + &self.scale_factor.view, + &self.rom.view, + &self.force_skip_bootrom.view, + ], + ); } } diff --git a/gb-emu/src/bin/macos/preferences/views/widgets.rs b/gb-emu/src/bin/macos/preferences/views/widgets.rs index 89152b3..0ab1648 100644 --- a/gb-emu/src/bin/macos/preferences/views/widgets.rs +++ b/gb-emu/src/bin/macos/preferences/views/widgets.rs @@ -1,6 +1,9 @@ +use std::fmt::Display; use std::marker::PhantomData; +use std::path::PathBuf; use cacao::button::Button; +use cacao::filesystem::FileSelectPanel; use cacao::input::TextField; use cacao::layout::{Layout, LayoutConstraint}; use cacao::select::Select; @@ -56,12 +59,15 @@ impl Default for ToggleView { view.add_subview(&title); LayoutConstraint::activate(&[ - switch.top.constraint_equal_to(&view.top), + switch.center_y.constraint_equal_to(&view.center_y), switch .leading .constraint_equal_to(&view.leading) .offset(LEFT_MARGIN), switch.width.constraint_equal_to_constant(24.), + title + .width + .constraint_greater_than_or_equal_to_constant(200.), title.top.constraint_equal_to(&view.top), title.leading.constraint_equal_to(&switch.trailing), title.trailing.constraint_equal_to(&view.trailing), @@ -90,10 +96,13 @@ impl ToggleView { pub struct PathView { pub view: View, pub field: TextField, - pub button: Button, + pub browse_button: Button, + pub clear_button: Button, pub title: Label, } +const PATH_VIEW_BUTTON_WIDTH: f64 = 80.; + impl Default for PathView { fn default() -> Self { let view = View::new(); @@ -104,8 +113,12 @@ impl Default for PathView { field.set_line_break_mode(cacao::text::LineBreakMode::TruncateMiddle); view.add_subview(&field); - let button = Button::new("Browse"); - view.add_subview(&button); + let browse_button = Button::new("Browse"); + view.add_subview(&browse_button); + + let clear_button = Button::new("Clear"); + view.add_subview(&clear_button); + let title = Label::new(); view.add_subview(&title); title.set_text_alignment(cacao::text::TextAlign::Right); @@ -122,33 +135,68 @@ impl Default for PathView { field.bottom.constraint_equal_to(&view.bottom), field .trailing - .constraint_equal_to(&button.leading) + .constraint_equal_to(&browse_button.leading) .offset(-10.), field .width .constraint_greater_than_or_equal_to_constant(200.), - button.center_y.constraint_equal_to(&view.center_y), - button.trailing.constraint_equal_to(&view.trailing), - button.width.constraint_equal_to_constant(100.), + browse_button.center_y.constraint_equal_to(&view.center_y), + browse_button + .trailing + .constraint_equal_to(&clear_button.leading) + .offset(-10.), + browse_button + .width + .constraint_equal_to_constant(PATH_VIEW_BUTTON_WIDTH), + clear_button.center_y.constraint_equal_to(&view.center_y), + clear_button.trailing.constraint_equal_to(&view.trailing), + clear_button + .width + .constraint_equal_to_constant(PATH_VIEW_BUTTON_WIDTH), ]); Self { view, field, - button, + browse_button, + clear_button, title, } } } impl PathView { - pub fn configure(&mut self, title: &str, placeholder: &str, state: Option) { + pub fn configure( + &mut self, + title: &str, + placeholder: &str, + state: Option, + handler: F, + ) where + F: Fn(Option) + Copy + Send + Sync + 'static, + { + self.browse_button.set_action(move |_v| { + let mut file_select_panel = FileSelectPanel::new(); + file_select_panel.set_can_choose_directories(false); + file_select_panel.set_can_choose_files(true); + file_select_panel.set_allows_multiple_selection(false); + file_select_panel.show(move |v| { + handler(v.first().map(|v| v.pathbuf())); + }); + }); + + self.clear_button.set_action(move |_v| handler(None)); + self.title.set_text(title); self.field.set_placeholder_text(placeholder); if let Some(state) = state { self.field.set_text(&state); } } + + pub fn update(&mut self, state: Option) { + self.field.set_text(&state.unwrap_or(String::from(""))); + } } pub struct PickerView @@ -301,4 +349,137 @@ impl StepperView { .set_text(format!("{:.1$}", val, self.decimal_places).as_str()); val } + + pub fn update_map(&mut self, map: F) -> T + where + F: Fn(f64) -> T, + T: Display, + { + let mapped = map(self.stepper.get_value()); + self.field.set_text(format!("{}", mapped).as_str()); + mapped + } +} + +pub struct StepperViewToggle { + pub view: View, + pub switch: Switch, + pub field: TextField, + pub stepper: Stepper, + pub title: Label, + pub decimal_places: usize, + pub enabled: bool, + pub suffix: String, +} + +impl Default for StepperViewToggle { + fn default() -> Self { + let view = View::new(); + + let stepper = Stepper::new(); + stepper.set_wraps(false); + view.add_subview(&stepper); + + let field = TextField::new(); + field.set_uses_single_line(true); + field.set_editable(false); + view.add_subview(&field); + + let title = Label::new(); + view.add_subview(&title); + title.set_text_alignment(cacao::text::TextAlign::Right); + + let switch = Switch::new(""); + view.add_subview(&switch); + + LayoutConstraint::activate(&[ + title.center_y.constraint_equal_to(&view.center_y), + title.leading.constraint_equal_to(&view.leading), + title.width.constraint_equal_to_constant(LEFT_MARGIN - 10.), + switch.center_y.constraint_equal_to(&view.center_y), + switch + .leading + .constraint_equal_to(&title.trailing) + .offset(10.), + field.top.constraint_equal_to(&view.top), + field + .leading + .constraint_equal_to(&switch.trailing) + .offset(10.), + stepper.center_y.constraint_equal_to(&view.center_y), + stepper + .leading + .constraint_equal_to(&field.trailing) + .offset(5.), + field.bottom.constraint_equal_to(&view.bottom), + field + .width + .constraint_greater_than_or_equal_to_constant(80.), + ]); + + Self { + view, + switch, + field, + stepper, + title, + decimal_places: 0, + enabled: false, + suffix: String::new(), + } + } +} + +impl StepperViewToggle { + #[allow(clippy::too_many_arguments)] + pub fn configure( + &mut self, + title: &str, + min: f64, + max: f64, + increment: f64, + decimal_places: usize, + initial_value: Option, + initial_enabled: bool, + stepper_handler: F, + switch_handler: G, + ) where + F: Fn(*const Object) + Send + Sync + 'static, + G: Fn(*const Object) + Send + Sync + 'static, + { + self.title.set_text(title); + self.decimal_places = decimal_places; + if let Some(val) = initial_value { + self.stepper.set_value(val); + } + self.stepper.set_min_value(min); + self.stepper.set_max_value(max); + self.stepper.set_increment(increment); + self.stepper.set_action(stepper_handler); + self.switch.set_action(switch_handler); + self.enabled = initial_enabled; + self.update(); + } + + pub fn set_suffix(&mut self, suffix: String) { + self.suffix = suffix; + } + + pub fn update(&mut self) -> Option { + self.switch.set_checked(self.enabled); + self.field.set_enabled(self.enabled); + if self.enabled { + let val = self.stepper.get_value(); + self.field + .set_text(format!("{:.1$}{2}", val, self.decimal_places, self.suffix,).as_str()); + + Some(val) + } else { + None + } + } + + pub fn flip(&mut self) { + self.enabled = !self.enabled; + } } diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index ba0534b..531f120 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -117,12 +117,21 @@ pub struct VulkanRendererConfig { pub cgb_resolution_override: ResolutionOverride, } -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] pub enum ResolutionOverride { Scale(usize), Default, } +impl From> for ResolutionOverride { + fn from(value: Option) -> Self { + match value { + Some(scale) => Self::Scale(scale), + None => Self::Default, + } + } +} + impl Default for Config { fn default() -> Self { Self {