diff --git a/Cargo.lock b/Cargo.lock index e2d1bc1..1e45d3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bitmask-enum" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fb8528abca6895a5ada33d62aedd538a5c33e77068256483b44a3230270163" +dependencies = [ + "quote", + "syn 2.0.38", +] + [[package]] name = "blake3" version = "1.5.0" @@ -439,6 +449,25 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "cacao" +version = "0.4.0-beta2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6de2bcb2324367ffb6ea53f977fab8234fa59e9a57e88f0f7be1ad7cb425c7b9" +dependencies = [ + "bitmask-enum", + "block", + "core-foundation 0.9.3", + "core-graphics 0.23.1", + "dispatch", + "lazy_static", + "libc", + "objc", + "objc_id", + "os_info", + "url", +] + [[package]] name = "cache-padded" version = "1.3.0" @@ -593,7 +622,7 @@ dependencies = [ "block", "core-foundation 0.7.0", "core-graphics 0.19.2", - "foreign-types", + "foreign-types 0.3.2", "libc", "objc", ] @@ -609,7 +638,7 @@ dependencies = [ "cocoa-foundation", "core-foundation 0.9.3", "core-graphics 0.22.3", - "foreign-types", + "foreign-types 0.3.2", "libc", "objc", ] @@ -712,7 +741,7 @@ checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" dependencies = [ "bitflags 1.3.2", "core-foundation 0.7.0", - "foreign-types", + "foreign-types 0.3.2", "libc", ] @@ -725,7 +754,20 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.3", "core-graphics-types", - "foreign-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.3", + "core-graphics-types", + "foreign-types 0.5.0", "libc", ] @@ -1134,7 +1176,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", ] [[package]] @@ -1143,6 +1206,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.29" @@ -1246,6 +1324,7 @@ name = "gb-emu" version = "0.5.0" dependencies = [ "bytemuck", + "cacao", "chrono", "clap", "cpal", @@ -1255,6 +1334,7 @@ dependencies = [ "gilrs", "image", "nokhwa", + "objc", "raw-window-handle", "send_wrapper", "serde", @@ -1527,6 +1607,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" version = "0.24.7" @@ -1989,7 +2079,7 @@ dependencies = [ "block", "cocoa 0.20.2", "core-graphics 0.19.2", - "foreign-types", + "foreign-types 0.3.2", "log", "objc", ] @@ -2003,7 +2093,7 @@ dependencies = [ "bitflags 1.3.2", "block", "core-graphics-types", - "foreign-types", + "foreign-types 0.3.2", "log", "objc", ] @@ -2499,6 +2589,15 @@ dependencies = [ "cc", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.1" @@ -2552,6 +2651,17 @@ dependencies = [ "redox_syscall 0.3.5", ] +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "serde", + "winapi", +] + [[package]] name = "owned_ttf_parser" version = "0.19.0" @@ -3391,6 +3501,21 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "toml" version = "0.7.8" @@ -3454,12 +3579,27 @@ dependencies = [ "wide", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.10.1" @@ -3478,6 +3618,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -3792,7 +3943,7 @@ dependencies = [ "block", "core-graphics-types", "d3d12", - "foreign-types", + "foreign-types 0.3.2", "fxhash", "glow", "gpu-alloc", diff --git a/gb-emu/Cargo.toml b/gb-emu/Cargo.toml index 84fa839..6877a17 100644 --- a/gb-emu/Cargo.toml +++ b/gb-emu/Cargo.toml @@ -6,6 +6,10 @@ description = "TWINC Game Boy (CGB/DMG) emulator" [package.metadata.bundle] identifier = "com.alexjanka.TWINC" +[package.metadata.bundle.bin.cli] +identifier = "com.alexjanka.TWINC.cli" +[package.metadata.bundle.bin.gui] +identifier = "com.alexjanka.TWINC.gui" [features] default = ["vulkan-static"] @@ -36,3 +40,7 @@ serde = { version = "1.0", features = ["derive"] } image = { version = "0.24", default-features = false, features = ["png"] } bytemuck = "1.14" chrono = "0.4" + +[target.'cfg(any(target_os = "macos"))'.dependencies] +cacao = "0.4.0-beta2" +objc = "0.2" diff --git a/gb-emu/src/bin/gui.rs b/gb-emu/src/bin/gui.rs new file mode 100644 index 0000000..bfa18f2 --- /dev/null +++ b/gb-emu/src/bin/gui.rs @@ -0,0 +1,7 @@ +#[cfg(target_os = "macos")] +mod macos; + +fn main() { + #[cfg(target_os = "macos")] + cacao::appkit::App::new("com.alexjanka.cacao-test", macos::TwincUiApp::default()).run(); +} diff --git a/gb-emu/src/bin/macos/mod.rs b/gb-emu/src/bin/macos/mod.rs new file mode 100644 index 0000000..71c3570 --- /dev/null +++ b/gb-emu/src/bin/macos/mod.rs @@ -0,0 +1,113 @@ +use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::window::{Window, WindowConfig, WindowStyle, WindowToolbarStyle}; +use cacao::appkit::{App, AppDelegate}; +use cacao::notification_center::Dispatcher; + +use self::preferences::{PreferencesMessage, PreferencesUi}; + +mod preferences; + +pub(crate) enum AppMessage { + Core(CoreMessage), + Preferences(PreferencesMessage), +} + +pub(crate) enum CoreMessage { + Open, + OpenPreferences, +} + +pub(crate) struct TwincUiApp { + preferences: Window<PreferencesUi>, +} + +impl Default for TwincUiApp { + fn default() -> Self { + Self { + preferences: Window::with( + { + let mut config = WindowConfig::default(); + config.set_initial_dimensions(100., 100., 400., 400.); + + config.set_styles(&[ + WindowStyle::Resizable, + WindowStyle::Miniaturizable, + WindowStyle::Closable, + WindowStyle::Titled, + ]); + + config.toolbar_style = WindowToolbarStyle::Preferences; + config + }, + PreferencesUi::new(), + ), + } + } +} + +impl AppDelegate for TwincUiApp { + fn did_finish_launching(&self) { + App::set_menu(menu()); + App::activate(); + } +} + +impl Dispatcher for TwincUiApp { + type Message = AppMessage; + + fn on_ui_message(&self, message: Self::Message) { + match message { + AppMessage::Core(CoreMessage::Open) => println!("open"), + AppMessage::Core(CoreMessage::OpenPreferences) => { + self.preferences.show(); + } + AppMessage::Preferences(prefs_message) => { + if let Some(delegate) = &self.preferences.delegate { + delegate.message(prefs_message) + } + } + } + } +} + +fn menu() -> Vec<Menu> { + vec![ + Menu::new( + "", + vec![ + MenuItem::About("Cacao Test".to_string()), + MenuItem::Separator, + MenuItem::new("Preferences") + .key(",") + .action(|| dispatch(AppMessage::Core(CoreMessage::OpenPreferences))), + MenuItem::Separator, + MenuItem::Services, + MenuItem::Separator, + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, + MenuItem::Separator, + MenuItem::Quit, + ], + ), + Menu::new( + "File", + vec![MenuItem::new("Open") + .key("o") + .action(|| dispatch(AppMessage::Core(CoreMessage::Open)))], + ), + Menu::new( + "Window", + vec![ + MenuItem::Minimize, + MenuItem::Separator, + MenuItem::new("Bring All to Front"), + ], + ), + Menu::new("Help", vec![]), + ] +} + +fn dispatch(message: AppMessage) { + App::<TwincUiApp, AppMessage>::dispatch_main(message); +} diff --git a/gb-emu/src/bin/macos/preferences.rs b/gb-emu/src/bin/macos/preferences.rs new file mode 100644 index 0000000..811f2c5 --- /dev/null +++ b/gb-emu/src/bin/macos/preferences.rs @@ -0,0 +1,80 @@ +use cacao::{ + appkit::{ + toolbar::Toolbar, + window::{Window, WindowDelegate}, + }, + view::ViewController, +}; + +use self::{ + toolbar::PreferencesToolbar, + views::{ + CorePreferencesContentView, StandalonePreferencesContentView, VstPreferencesContentView, + }, +}; + +mod toolbar; +mod views; + +pub(crate) enum PreferencesMessage { + SwitchPane(PreferencesPane), +} + +pub(crate) enum PreferencesPane { + Core, + Standalone, + Vst, +} + +pub(crate) struct PreferencesUi { + pub(crate) toolbar: Toolbar<PreferencesToolbar>, + pub(crate) core_prefs: ViewController<CorePreferencesContentView>, + pub(crate) standalone_prefs: ViewController<StandalonePreferencesContentView>, + pub(crate) vst_prefs: ViewController<VstPreferencesContentView>, + window: Option<Window>, +} + +impl PreferencesUi { + pub(crate) fn new() -> Self { + Self { + toolbar: Toolbar::new("PreferencesToolbar", PreferencesToolbar::default()), + core_prefs: ViewController::new(CorePreferencesContentView::default()), + standalone_prefs: ViewController::new(StandalonePreferencesContentView::default()), + vst_prefs: ViewController::new(VstPreferencesContentView::default()), + window: None, + } + } + + pub(crate) fn message(&self, message: PreferencesMessage) { + let window = self.window.as_ref().unwrap(); + + match message { + PreferencesMessage::SwitchPane(PreferencesPane::Core) => { + window.set_title("Core"); + window.set_content_view_controller(&self.core_prefs); + } + PreferencesMessage::SwitchPane(PreferencesPane::Standalone) => { + window.set_title("Standalone"); + window.set_content_view_controller(&self.standalone_prefs); + } + PreferencesMessage::SwitchPane(PreferencesPane::Vst) => { + window.set_title("VST"); + window.set_content_view_controller(&self.vst_prefs); + } + } + } +} + +impl WindowDelegate for PreferencesUi { + const NAME: &'static str = "PreferencesUi"; + + fn did_load(&mut self, window: Window) { + window.set_autosave_name("PreferencesWindow"); + window.set_movable_by_background(true); + window.set_toolbar(&self.toolbar); + + self.window = Some(window); + + self.message(PreferencesMessage::SwitchPane(PreferencesPane::Core)); + } +} diff --git a/gb-emu/src/bin/macos/preferences/toolbar.rs b/gb-emu/src/bin/macos/preferences/toolbar.rs new file mode 100644 index 0000000..af7293b --- /dev/null +++ b/gb-emu/src/bin/macos/preferences/toolbar.rs @@ -0,0 +1,109 @@ +use cacao::{ + appkit::toolbar::{ItemIdentifier, ToolbarDelegate, ToolbarItem}, + image::{Image, MacSystemIcon}, +}; + +use crate::macos::{dispatch, AppMessage}; + +use super::{PreferencesMessage, PreferencesPane}; + +pub(crate) struct PreferencesToolbar { + core: ToolbarItem, + standalone: ToolbarItem, + vst: ToolbarItem, +} + +impl Default for PreferencesToolbar { + fn default() -> Self { + Self { + core: { + let mut item = ToolbarItem::new("core"); + item.set_title("Core"); + + let icon = Image::toolbar_icon(MacSystemIcon::PreferencesGeneral, "Core"); + item.set_image(icon); + + item.set_action(|_| { + dispatch(AppMessage::Preferences(PreferencesMessage::SwitchPane( + PreferencesPane::Core, + ))); + }); + + item + }, + standalone: { + let mut item = ToolbarItem::new("standalone"); + item.set_title("Standalone"); + + let icon = Image::toolbar_icon(MacSystemIcon::PreferencesGeneral, "Standalone"); + item.set_image(icon); + + item.set_action(|_| { + dispatch(AppMessage::Preferences(PreferencesMessage::SwitchPane( + PreferencesPane::Standalone, + ))); + }); + + item + }, + vst: { + let mut item = ToolbarItem::new("vst"); + item.set_title("VST"); + + let icon = Image::toolbar_icon(MacSystemIcon::PreferencesGeneral, "VST"); + item.set_image(icon); + + item.set_action(|_| { + dispatch(AppMessage::Preferences(PreferencesMessage::SwitchPane( + PreferencesPane::Vst, + ))); + }); + + item + }, + } + } +} + +impl ToolbarDelegate for PreferencesToolbar { + const NAME: &'static str = "PreferencesToolbar"; + + fn did_load(&mut self, toolbar: cacao::appkit::toolbar::Toolbar) { + toolbar.set_selected("core") + } + + fn allowed_item_identifiers(&self) -> Vec<cacao::appkit::toolbar::ItemIdentifier> { + vec![ + ItemIdentifier::Custom("core"), + ItemIdentifier::Custom("standalone"), + ItemIdentifier::Custom("vst"), + ] + } + + fn default_item_identifiers(&self) -> Vec<cacao::appkit::toolbar::ItemIdentifier> { + vec![ + ItemIdentifier::Custom("core"), + ItemIdentifier::Custom("standalone"), + ItemIdentifier::Custom("vst"), + ] + } + + fn selectable_item_identifiers(&self) -> Vec<ItemIdentifier> { + vec![ + ItemIdentifier::Custom("core"), + ItemIdentifier::Custom("standalone"), + ItemIdentifier::Custom("vst"), + ] + } + + fn item_for(&self, identifier: &str) -> &cacao::appkit::toolbar::ToolbarItem { + match identifier { + "core" => &self.core, + "standalone" => &self.standalone, + "vst" => &self.vst, + _ => { + unreachable!(); + } + } + } +} diff --git a/gb-emu/src/bin/macos/preferences/views.rs b/gb-emu/src/bin/macos/preferences/views.rs new file mode 100644 index 0000000..5dd13e2 --- /dev/null +++ b/gb-emu/src/bin/macos/preferences/views.rs @@ -0,0 +1,65 @@ +use cacao::{ + layout::{Layout, LayoutConstraint}, + view::{View, ViewDelegate}, +}; + +use self::widgets::ToggleOptionView; + +mod widgets; + +#[derive(Default)] +pub(crate) struct CorePreferencesContentView { + pub example_option: ToggleOptionView, +} + +impl ViewDelegate for CorePreferencesContentView { + const NAME: &'static str = "CorePreferencesContentView"; + + fn did_load(&mut self, view: View) { + self.example_option.configure( + "An example preference", + "This can be true, or it can be false.", + false, // initial value + |_v| {}, + ); + + view.add_subview(&self.example_option.view); + + LayoutConstraint::activate(&[ + self.example_option + .view + .top + .constraint_equal_to(&view.top) + .offset(22.), + self.example_option + .view + .leading + .constraint_equal_to(&view.leading) + .offset(22.), + self.example_option + .view + .trailing + .constraint_equal_to(&view.trailing) + .offset(-22.), + self.example_option + .view + .bottom + .constraint_equal_to(&view.bottom) + .offset(-22.), + ]); + } +} + +#[derive(Default)] +pub(crate) struct StandalonePreferencesContentView {} + +impl ViewDelegate for StandalonePreferencesContentView { + const NAME: &'static str = "StandalonePreferencesContentView"; +} + +#[derive(Default)] +pub(crate) struct VstPreferencesContentView {} + +impl ViewDelegate for VstPreferencesContentView { + const NAME: &'static str = "VstPreferencesContentView"; +} diff --git a/gb-emu/src/bin/macos/preferences/views/widgets.rs b/gb-emu/src/bin/macos/preferences/views/widgets.rs new file mode 100644 index 0000000..57b88bf --- /dev/null +++ b/gb-emu/src/bin/macos/preferences/views/widgets.rs @@ -0,0 +1,63 @@ +use cacao::layout::{Layout, LayoutConstraint}; +use cacao::switch::Switch; +use cacao::text::Label; +use cacao::view::View; +use objc::runtime::Object; + +#[derive(Debug)] +pub struct ToggleOptionView { + pub view: View, + pub switch: Switch, + pub title: Label, + pub subtitle: Label, +} + +impl Default for ToggleOptionView { + fn default() -> Self { + let view = View::new(); + + let switch = Switch::new(""); + view.add_subview(&switch); + + let title = Label::new(); + view.add_subview(&title); + + let subtitle = Label::new(); + view.add_subview(&subtitle); + + LayoutConstraint::activate(&[ + switch.top.constraint_equal_to(&view.top), + switch.leading.constraint_equal_to(&view.leading), + switch.width.constraint_equal_to_constant(24.), + title.top.constraint_equal_to(&view.top), + title.leading.constraint_equal_to(&switch.trailing), + title.trailing.constraint_equal_to(&view.trailing), + subtitle.top.constraint_equal_to(&title.bottom), + subtitle.leading.constraint_equal_to(&switch.trailing), + subtitle.trailing.constraint_equal_to(&view.trailing), + subtitle.bottom.constraint_equal_to(&view.bottom), + subtitle + .width + .constraint_greater_than_or_equal_to_constant(200.), + ]); + + ToggleOptionView { + view, + switch, + title, + subtitle, + } + } +} + +impl ToggleOptionView { + pub fn configure<F>(&mut self, text: &str, subtitle: &str, state: bool, handler: F) + where + F: Fn(*const Object) + Send + Sync + 'static, + { + self.title.set_text(text); + self.subtitle.set_text(subtitle); + self.switch.set_action(handler); + self.switch.set_checked(state); + } +}