diff --git a/BUILD.md b/BUILD.md index d7b0de78..85d51bf7 100644 --- a/BUILD.md +++ b/BUILD.md @@ -226,7 +226,7 @@ Example usage: `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DENABLE_SDL=ON - | ENABLE_SDL | | Enable SDLController controller API | ON | Currently required | | ENABLE_VCPKG | | Use VCPKG package manager to obtain dependencies | ON | | | ENABLE_VULKAN | | Enable the Vulkan graphics backend | ON | | -| ENABLE_WXWIDGETS | | Enable wxWidgets UI | ON | Currently required | +| ENABLE_WXWIDGETS | | Enable wxWidgets UI | ON | | ### Windows | Flag | Description | Default | Note | @@ -244,6 +244,7 @@ Example usage: `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DENABLE_SDL=ON - | ENABLE_WAYLAND | Enable Wayland support | ON | ### macOS -| Flag | Description | Default | -|--------------|------------------------------------------------|---------| -| MACOS_BUNDLE | MacOS executable will be an application bundle | OFF | +| Flag | Description | Default | +|---------------------|------------------------------------------------------|---------| +| ENABLE_SWIFTUI_MACOS | Enable experimental native macOS SwiftUI GUI backend | OFF | +| MACOS_BUNDLE | MacOS executable will be an application bundle | OFF | diff --git a/CMakeLists.txt b/CMakeLists.txt index a00879fe..bf00c1be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ execute_process( OUTPUT_VARIABLE GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ) -add_definitions(-DEMULATOR_HASH=${GIT_HASH}) +add_compile_definitions($<$:EMULATOR_HASH=${GIT_HASH}>) if (ENABLE_VCPKG) # check if vcpkg is shallow and unshallow it if necessary @@ -66,9 +66,9 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_compile_definitions($<$:CEMU_DEBUG_ASSERT>) # if build type is debug, set CEMU_DEBUG_ASSERT -add_definitions(-DEMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR}) -add_definitions(-DEMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR}) -add_definitions(-DEMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}) +add_compile_definitions($<$:EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR}>) +add_compile_definitions($<$:EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR}>) +add_compile_definitions($<$:EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}>) set_property(GLOBAL PROPERTY USE_FOLDERS ON) @@ -194,7 +194,7 @@ endif() if (ENABLE_METAL) include_directories(${CMAKE_SOURCE_DIR}/dependencies/metal-cpp) - add_definitions(-DENABLE_METAL=1) + add_compile_definitions($<$:ENABLE_METAL=1>) endif() if (ENABLE_DISCORD_RPC) @@ -232,7 +232,7 @@ if (ENABLE_CUBEB) set_property(TARGET cubeb PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") add_library(cubeb::cubeb ALIAS cubeb) endif() - add_compile_definitions("HAS_CUBEB=1") + add_compile_definitions($<$:HAS_CUBEB=1>) endif() add_subdirectory("dependencies/ih264d" EXCLUDE_FROM_ALL) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b7fd67fe..37ddae52 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,7 +32,13 @@ elseif(UNIX) add_compile_options(-Wno-ambiguous-reversed-operator) endif() - add_compile_options(-Wno-multichar -Wno-invalid-offsetof -Wno-switch -Wno-ignored-attributes -Wno-deprecated-enum-enum-conversion) + add_compile_options( + $<$:-Wno-multichar> + $<$:-Wno-invalid-offsetof> + $<$:-Wno-switch> + $<$:-Wno-ignored-attributes> + $<$:-Wno-deprecated-enum-enum-conversion> + ) endif() add_compile_definitions(VK_NO_PROTOTYPES) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index d66e971a..b182aa84 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,7 +1,12 @@ add_library(CemuGui INTERFACE) target_include_directories(CemuGui INTERFACE "interface") -if(ENABLE_WXWIDGETS) +if(APPLE AND ENABLE_SWIFTUI_MACOS) + add_subdirectory(swiftui) + target_link_libraries(CemuGui INTERFACE CemuSwiftUiGui) +elseif(ENABLE_WXWIDGETS) add_subdirectory(wxgui) target_link_libraries(CemuGui INTERFACE CemuWxGui) +else() + message(FATAL_ERROR "No GUI backend selected. Enable ENABLE_WXWIDGETS or ENABLE_SWIFTUI_MACOS on macOS.") endif() diff --git a/src/gui/swiftui/CMakeLists.txt b/src/gui/swiftui/CMakeLists.txt new file mode 100644 index 00000000..8e011244 --- /dev/null +++ b/src/gui/swiftui/CMakeLists.txt @@ -0,0 +1,29 @@ +enable_language(Swift) + +add_library(CemuSwiftUiGui STATIC + WindowSystemSwiftUI.mm + ContentView.swift +) + +set_target_properties(CemuSwiftUiGui PROPERTIES + Swift_LANGUAGE_VERSION 5 +) + +set_property(TARGET CemuSwiftUiGui PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +target_include_directories(CemuSwiftUiGui PRIVATE "../../") +target_include_directories(CemuSwiftUiGui PUBLIC "../") + +target_link_libraries(CemuSwiftUiGui PRIVATE + CemuCommon + CemuConfig + CemuCafe + CemuResource +) + +find_library(COCOA_FRAMEWORK Cocoa REQUIRED) +target_link_libraries(CemuSwiftUiGui PRIVATE ${COCOA_FRAMEWORK}) + +if(ALLOW_PORTABLE) + target_compile_definitions(CemuSwiftUiGui PRIVATE CEMU_ALLOW_PORTABLE) +endif() diff --git a/src/gui/swiftui/ContentView.swift b/src/gui/swiftui/ContentView.swift new file mode 100644 index 00000000..2b5e247c --- /dev/null +++ b/src/gui/swiftui/ContentView.swift @@ -0,0 +1,237 @@ +import AppKit +import SwiftUI + +@objc(CemuSwiftUIRootViewController) +final class CemuSwiftUIRootViewController: NSViewController { + override func loadView() { + self.view = NSHostingView(rootView: ContentView()) + } +} + +@_cdecl("CemuCreateSwiftUIRootViewController") +public func CemuCreateSwiftUIRootViewController() -> UnsafeMutableRawPointer { + let controller = CemuSwiftUIRootViewController() + return Unmanaged.passRetained(controller).autorelease().toOpaque() +} + +struct ContentView: View { + @State private var selectedTitleID: UInt64? + @State private var showUpdatingBanner = false + @State private var games: [GameItem] = [ + GameItem( + titleID: 0x0005_0000_101C_9400, + name: "The Legend of Zelda: Breath of the Wild", + version: "208", + dlc: "80", + played: "37 hours 42 minutes", + lastPlayed: "4/12/26", + region: "EUR" + ), + GameItem( + titleID: 0x0005_0000_1010_EC00, + name: "Mario Kart 8", + version: "64", + dlc: "48", + played: "12 hours 5 minutes", + lastPlayed: "3/28/26", + region: "USA" + ), + GameItem( + titleID: 0x0005_0000_1017_6A00, + name: "Splatoon", + version: "80", + dlc: "", + played: "1 hour 12 minutes", + lastPlayed: "never", + region: "JPN" + ), + GameItem( + titleID: 0x0005_0000_1014_4F00, + name: "Super Smash Bros. for Wii U", + version: "288", + dlc: "192", + played: "", + lastPlayed: "never", + region: "USA" + ), + ] + + var body: some View { + VStack(spacing: 0) { + GameListHeaderView() + + Divider() + + ScrollView { + LazyVStack(spacing: 0) { + ForEach(Array(games.enumerated()), id: \.element.id) { index, game in + GameListRowView( + game: game, + isSelected: selectedTitleID == game.titleID, + isAlternateRow: index.isMultiple(of: 2) + ) + .contentShape(Rectangle()) + .onTapGesture { + selectedTitleID = game.titleID + } + + Divider() + } + } + } + .background(Color(nsColor: .controlBackgroundColor)) + + if showUpdatingBanner { + GameListInfoBarView(message: "Updating game list...") { + withAnimation(.easeInOut(duration: 0.2)) { + showUpdatingBanner = false + } + } + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + } + .toolbar { + ToolbarItemGroup(placement: .automatic) { + Button(action: refreshGameList) { + Image(systemName: "arrow.clockwise") + } + .help("Refresh game list") + + Button(action: openSettings) { + Image(systemName: "gear") + } + .help("Settings") + } + } + .frame(minWidth: 900, minHeight: 480) + } + + private func refreshGameList() { + withAnimation(.easeInOut(duration: 0.2)) { + showUpdatingBanner = true + } + } + + private func openSettings() { + // SwiftUI migration placeholder: the menu action exists in WindowSystemSwiftUI. + } +} + +struct GameListHeaderView: View { + var body: some View { + HStack(spacing: 0) { + headerCell("Icon", width: 66, alignment: .center) + headerCell("Game", width: 340, alignment: .leading) + headerCell("Version", width: 84, alignment: .leading) + headerCell("DLC", width: 68, alignment: .leading) + headerCell("You've played", width: 170, alignment: .leading) + headerCell("Last played", width: 136, alignment: .leading) + headerCell("Region", width: 88, alignment: .leading) + headerCell("Title ID", width: 170, alignment: .leading) + } + .padding(.vertical, 4) + .background(Color(nsColor: .windowBackgroundColor)) + } + + private func headerCell(_ title: String, width: CGFloat, alignment: Alignment) -> some View { + Text(title) + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.primary) + .lineLimit(1) + .frame(width: width, alignment: alignment) + .padding(.horizontal, 8) + } +} + +struct GameListRowView: View { + let game: GameItem + let isSelected: Bool + let isAlternateRow: Bool + + var body: some View { + HStack(spacing: 0) { + iconCell + .frame(width: 66, alignment: .center) + + textCell(game.name, width: 340) + textCell(game.version, width: 84) + textCell(game.dlc, width: 68) + textCell(game.played, width: 170) + textCell(game.lastPlayed, width: 136) + textCell(game.region, width: 88) + textCell(String(format: "%016llx", game.titleID), width: 170) + } + .frame(maxWidth: .infinity, minHeight: 40, alignment: .leading) + .background(backgroundColor) + } + + private var iconCell: some View { + RoundedRectangle(cornerRadius: 4) + .fill(Color.accentColor.opacity(0.20)) + .frame(width: 40, height: 24) + .overlay( + Image(systemName: "photo") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(.secondary) + ) + .padding(.horizontal, 8) + } + + private func textCell(_ value: String, width: CGFloat) -> some View { + Text(value) + .font(.system(size: 12)) + .lineLimit(1) + .truncationMode(.tail) + .frame(width: width, alignment: .leading) + .padding(.horizontal, 8) + } + + private var backgroundColor: Color { + if isSelected { + return Color(nsColor: .selectedContentBackgroundColor) + } + + if isAlternateRow { + return Color(nsColor: .controlBackgroundColor) + } + + return Color(nsColor: .windowBackgroundColor) + } +} + +struct GameListInfoBarView: View { + let message: String + let onDismiss: () -> Void + + var body: some View { + HStack(spacing: 10) { + Image(systemName: "arrow.triangle.2.circlepath") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.secondary) + + Text(message) + .font(.system(size: 12)) + + Spacer() + + Button("Dismiss", action: onDismiss) + .buttonStyle(.plain) + .font(.system(size: 12, weight: .medium)) + } + .padding(.horizontal, 10) + .padding(.vertical, 7) + .background(Color(nsColor: .unemphasizedSelectedContentBackgroundColor)) + } +} + +struct GameItem: Identifiable { + let titleID: UInt64 + let name: String + let version: String + let dlc: String + let played: String + let lastPlayed: String + let region: String + + var id: UInt64 { titleID } +} diff --git a/src/gui/swiftui/WindowSystemSwiftUI.mm b/src/gui/swiftui/WindowSystemSwiftUI.mm new file mode 100644 index 00000000..cdbb5cb8 --- /dev/null +++ b/src/gui/swiftui/WindowSystemSwiftUI.mm @@ -0,0 +1,389 @@ +#include "Common/precompiled.h" + +#include "interface/WindowSystem.h" + +#include "Cafe/CafeSystem.h" +#include "Cafe/TitleList/TitleInfo.h" +#include "Cafe/TitleList/TitleList.h" +#include "config/ActiveSettings.h" + +#import +#import + +extern "C" void *CemuCreateSwiftUIRootViewController(void); + +@interface CemuAppDelegate : NSObject +- (void)setupMenuBar; +- (void)quitApp:(id)sender; +- (void)openGame:(id)sender; +- (void)openPreferences:(id)sender; +- (void)toggleFullscreen:(id)sender; +- (void)showHelp:(id)sender; +- (void)showAbout:(id)sender; +@end + +namespace { +extern WindowSystem::WindowInfo g_window_info; +extern NSWindow *g_main_window; +extern CemuAppDelegate *g_app_delegate; +bool PrepareLaunchPath(const fs::path &launchPath, std::string &errorOut); +} + +@implementation CemuAppDelegate + +- (void)setupMenuBar { + NSMenu *mainMenu = [[NSMenu alloc] init]; + + // File menu + NSMenu *fileMenu = [[NSMenu alloc] initWithTitle:@"File"]; + NSMenuItem *fileMenuItem = [mainMenu addItemWithTitle:@"File" action:nil keyEquivalent:@""]; + [mainMenu setSubmenu:fileMenu forItem:fileMenuItem]; + + NSMenuItem *openGameItem = [fileMenu addItemWithTitle:@"Open Game..." action:@selector(openGame:) keyEquivalent:@"o"]; + [openGameItem setTarget:self]; + [fileMenu addItem:[NSMenuItem separatorItem]]; + NSMenuItem *quitItem = [fileMenu addItemWithTitle:@"Quit Cemu" action:@selector(quitApp:) keyEquivalent:@"q"]; + [quitItem setTarget:self]; + + // Edit menu + NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"]; + NSMenuItem *editMenuItem = [mainMenu addItemWithTitle:@"Edit" action:nil keyEquivalent:@""]; + [mainMenu setSubmenu:editMenu forItem:editMenuItem]; + + NSMenuItem *preferencesItem = [editMenu addItemWithTitle:@"Preferences..." action:@selector(openPreferences:) keyEquivalent:@","]; + [preferencesItem setTarget:self]; + + // View menu + NSMenu *viewMenu = [[NSMenu alloc] initWithTitle:@"View"]; + NSMenuItem *viewMenuItem = [mainMenu addItemWithTitle:@"View" action:nil keyEquivalent:@""]; + [mainMenu setSubmenu:viewMenu forItem:viewMenuItem]; + + NSMenuItem *fullscreenItem = [viewMenu addItemWithTitle:@"Toggle Fullscreen" action:@selector(toggleFullscreen:) keyEquivalent:@"f"]; + [fullscreenItem setTarget:self]; + + // Help menu + NSMenu *helpMenu = [[NSMenu alloc] initWithTitle:@"Help"]; + NSMenuItem *helpMenuItem = [mainMenu addItemWithTitle:@"Help" action:nil keyEquivalent:@""]; + [mainMenu setSubmenu:helpMenu forItem:helpMenuItem]; + + NSMenuItem *helpItem = [helpMenu addItemWithTitle:@"Cemu Help" action:@selector(showHelp:) keyEquivalent:@""]; + [helpItem setTarget:self]; + NSMenuItem *aboutItem = [helpMenu addItemWithTitle:@"About Cemu" action:@selector(showAbout:) keyEquivalent:@""]; + [aboutItem setTarget:self]; + + [NSApp setMainMenu:mainMenu]; +} + +- (void)quitApp:(id)sender { + [NSApp terminate:sender]; +} + +- (void)openGame:(id)sender { + if (CafeSystem::IsTitleRunning()) { + WindowSystem::ShowErrorDialog("A title is already running.", + "Launch blocked"); + return; + } + + NSOpenPanel *panel = [NSOpenPanel openPanel]; + [panel setCanChooseFiles:YES]; + [panel setCanChooseDirectories:NO]; + [panel setAllowsMultipleSelection:NO]; + [panel setAllowedContentTypes:@[ + [UTType typeWithFilenameExtension:@"wud"], + [UTType typeWithFilenameExtension:@"wux"], + [UTType typeWithFilenameExtension:@"wua"], + [UTType typeWithFilenameExtension:@"wuhb"], + [UTType typeWithFilenameExtension:@"iso"], + [UTType typeWithFilenameExtension:@"rpx"], + [UTType typeWithFilenameExtension:@"elf"], + [UTType typeWithFilenameExtension:@"tmd"] + ]]; + + if ([panel runModal] != NSModalResponseOK || panel.URL == nil) + return; + + NSString *pathString = panel.URL.path; + if (pathString.length == 0) + return; + + fs::path launchPath = _utf8ToPath(std::string(pathString.UTF8String)); + std::string errorMessage; + if (!PrepareLaunchPath(launchPath, errorMessage)) { + WindowSystem::ShowErrorDialog(errorMessage, "Failed to launch game"); + return; + } + + WindowSystem::UpdateWindowTitles(false, true, 0.0); + CafeSystem::LaunchForegroundTitle(); + WindowSystem::NotifyGameLoaded(); + + const std::string titleName = CafeSystem::GetForegroundTitleName(); + if (!titleName.empty() && g_main_window) { + [g_main_window setTitle:[NSString stringWithUTF8String:titleName.c_str()]]; + } +} + +- (void)openPreferences:(id)sender { + fs::path configPath = ActiveSettings::GetConfigPath(); + std::string configPathUtf8 = _pathToUtf8(configPath); + NSString *configNSString = + [NSString stringWithUTF8String:configPathUtf8.c_str()]; + if (configNSString.length == 0) + return; + + NSURL *configURL = [NSURL fileURLWithPath:configNSString isDirectory:YES]; + [[NSWorkspace sharedWorkspace] openURL:configURL]; +} + +- (void)toggleFullscreen:(id)sender { + if (!g_main_window) + return; + + [g_main_window toggleFullScreen:nil]; + g_window_info.is_fullscreen = !g_window_info.is_fullscreen.load(); +} + +- (void)showHelp:(id)sender { + NSURL *helpURL = [NSURL URLWithString:@"https://wiki.cemu.info"]; + if (helpURL) + [[NSWorkspace sharedWorkspace] openURL:helpURL]; +} + +- (void)showAbout:(id)sender { + NSAlert *about = [[NSAlert alloc] init]; + [about setAlertStyle:NSAlertStyleInformational]; + [about setMessageText:@"About Cemu"]; + [about setInformativeText:@"Cemu - Wii U Emulator\nSwiftUI macOS GUI"]; + [about addButtonWithTitle:@"OK"]; + [about runModal]; +} + +@end + +namespace { +WindowSystem::WindowInfo g_window_info{}; +NSWindow *g_main_window = nil; +CemuAppDelegate *g_app_delegate = nil; + +bool PrepareLaunchPath(const fs::path &launchPath, std::string &errorOut) { + TitleInfo launchTitle{launchPath}; + if (launchTitle.IsValid()) { + CafeTitleList::AddTitleFromPath(launchPath); + + TitleId baseTitleId; + if (!CafeTitleList::FindBaseTitleId(launchTitle.GetAppTitleId(), + baseTitleId)) { + errorOut = + "Unable to launch game because the base files were not found."; + return false; + } + + CafeSystem::PREPARE_STATUS_CODE status = + CafeSystem::PrepareForegroundTitle(baseTitleId); + if (status == CafeSystem::PREPARE_STATUS_CODE::UNABLE_TO_MOUNT) { + errorOut = + "Unable to mount title. Make sure your game paths are valid and " + "refresh the game list."; + return false; + } + if (status != CafeSystem::PREPARE_STATUS_CODE::SUCCESS) { + errorOut = "Failed to prepare the selected game for launch."; + return false; + } + return true; + } + + CafeTitleFileType fileType = DetermineCafeSystemFileType(launchPath); + if (fileType == CafeTitleFileType::RPX || fileType == CafeTitleFileType::ELF) { + CafeSystem::PREPARE_STATUS_CODE status = + CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath); + if (status != CafeSystem::PREPARE_STATUS_CODE::SUCCESS) { + errorOut = "Failed to prepare standalone RPX/ELF executable."; + return false; + } + return true; + } + + errorOut = "Unsupported or invalid Wii U title path."; + return false; +} +} // namespace + +void WindowSystem::ShowErrorDialog( + std::string_view message, std::string_view title, + std::optional /*errorCategory*/) { + @autoreleasepool { + NSAlert *alert = [[NSAlert alloc] init]; + std::string titleCopy(title); + NSString *alertTitle = + titleCopy.empty() ? @"Error" + : [NSString stringWithUTF8String:titleCopy.c_str()]; + NSString *alertMessage = + [NSString stringWithUTF8String:std::string(message).c_str()]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert setMessageText:alertTitle]; + [alert setInformativeText:alertMessage ?: @""]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + } +} + +void WindowSystem::Create() { + @autoreleasepool { + NSApplication *app = [NSApplication sharedApplication]; + [app setActivationPolicy:NSApplicationActivationPolicyRegular]; + + // Setup application delegate with menu bar + g_app_delegate = [[CemuAppDelegate alloc] init]; + [app setDelegate:g_app_delegate]; + [g_app_delegate setupMenuBar]; + + const NSRect frame = NSMakeRect(120.0, 120.0, 1280.0, 720.0); + const NSWindowStyleMask style = + NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable | + NSWindowStyleMaskUnifiedTitleAndToolbar; + + g_main_window = [[NSWindow alloc] initWithContentRect:frame + styleMask:style + backing:NSBackingStoreBuffered + defer:NO]; + [g_main_window setTitle:@"Cemu"]; + [g_main_window setTitlebarAppearsTransparent:NO]; + + // Instantiate SwiftUI-backed root controller via explicit Swift C symbol. + NSViewController *rootViewController = nil; + if (void *swiftControllerPtr = CemuCreateSwiftUIRootViewController()) { + id swiftControllerObj = (__bridge id)swiftControllerPtr; + if ([swiftControllerObj isKindOfClass:[NSViewController class]]) { + rootViewController = (NSViewController *)swiftControllerObj; + } + } + if (!rootViewController) { + rootViewController = [[NSViewController alloc] init]; + NSView *contentView = [[NSView alloc] initWithFrame:frame]; + [contentView setWantsLayer:YES]; + contentView.layer.backgroundColor = NSColor.windowBackgroundColor.CGColor; + + NSTextField *fallbackLabel = [NSTextField labelWithString: + @"SwiftUI root view not found.\nUsing AppKit fallback view."]; + [fallbackLabel setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightMedium]]; + [fallbackLabel setTextColor:NSColor.secondaryLabelColor]; + [fallbackLabel setAlignment:NSTextAlignmentCenter]; + [fallbackLabel setFrame:NSMakeRect(40, frame.size.height / 2 - 20, + frame.size.width - 80, 60)]; + [fallbackLabel setAutoresizingMask:NSViewWidthSizable | NSViewMinYMargin | NSViewMaxYMargin]; + [contentView addSubview:fallbackLabel]; + + rootViewController.view = contentView; + } + + [g_main_window setContentViewController:rootViewController]; + + [g_main_window makeKeyAndOrderFront:nil]; + [app activateIgnoringOtherApps:YES]; + + g_window_info.app_active = true; + g_window_info.width = static_cast(frame.size.width); + g_window_info.height = static_cast(frame.size.height); + g_window_info.phys_width = g_window_info.width.load(); + g_window_info.phys_height = g_window_info.height.load(); + g_window_info.dpi_scale = 1.0; + g_window_info.pad_open = false; + g_window_info.pad_width = 0; + g_window_info.pad_height = 0; + g_window_info.phys_pad_width = 0; + g_window_info.phys_pad_height = 0; + g_window_info.pad_dpi_scale = 1.0; + g_window_info.is_fullscreen = false; + g_window_info.debugger_focused = false; + g_window_info.window_main.backend = + WindowSystem::WindowHandleInfo::Backend::Cocoa; + g_window_info.window_main.display = nullptr; + g_window_info.window_main.surface = (__bridge void *)g_main_window; + + [NSApp run]; + } +} + +WindowSystem::WindowInfo &WindowSystem::GetWindowInfo() { + return g_window_info; +} + +void WindowSystem::UpdateWindowTitles(bool isIdle, bool isLoading, double fps) { + if (!g_main_window) + return; + + NSString *title = nil; + if (isIdle) + title = @"Cemu"; + else if (isLoading) + title = @"Cemu - Loading..."; + else + title = [NSString stringWithFormat:@"Cemu - FPS: %.2f", fps]; + + [g_main_window setTitle:title]; +} + +void WindowSystem::GetWindowSize(int &w, int &h) { + w = g_window_info.width; + h = g_window_info.height; +} + +void WindowSystem::GetPadWindowSize(int &w, int &h) { + w = 0; + h = 0; +} + +void WindowSystem::GetWindowPhysSize(int &w, int &h) { + w = g_window_info.phys_width; + h = g_window_info.phys_height; +} + +void WindowSystem::GetPadWindowPhysSize(int &w, int &h) { + w = 0; + h = 0; +} + +double WindowSystem::GetWindowDPIScale() { return g_window_info.dpi_scale; } + +double WindowSystem::GetPadDPIScale() { return 1.0; } + +bool WindowSystem::IsPadWindowOpen() { return false; } + +bool WindowSystem::IsKeyDown(uint32 key) { + return g_window_info.get_keystate(key); +} + +bool WindowSystem::IsKeyDown(PlatformKeyCodes key) { + switch (key) { + case PlatformKeyCodes::LCONTROL: + return IsKeyDown(0x3B); + case PlatformKeyCodes::RCONTROL: + return IsKeyDown(0x3E); + case PlatformKeyCodes::TAB: + return IsKeyDown(0x30); + case PlatformKeyCodes::ESCAPE: + return IsKeyDown(0x35); + default: + return false; + } +} + +std::string WindowSystem::GetKeyCodeName(uint32 key) { + return fmt::format("key_{}", key); +} + +bool WindowSystem::InputConfigWindowHasFocus() { return false; } + +void WindowSystem::NotifyGameLoaded() {} + +void WindowSystem::NotifyGameExited() {} + +void WindowSystem::RefreshGameList() { CafeTitleList::Refresh(); } + +void WindowSystem::CaptureInput(const ControllerState & /*currentState*/, + const ControllerState & /*lastState*/) {} + +bool WindowSystem::IsFullScreen() { return g_window_info.is_fullscreen; }