mirror of
https://github.com/cemu-project/Cemu.git
synced 2026-04-25 20:26:19 -06:00
stop my swiftui fork leaking...
This commit is contained in:
parent
21e69d03b5
commit
37683f5e94
5
BUILD.md
5
BUILD.md
@ -244,7 +244,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 | |
|
||||
| ENABLE_WXWIDGETS | | Enable wxWidgets UI | ON | Currently required |
|
||||
|
||||
### Windows
|
||||
| Flag | Description | Default | Note |
|
||||
@ -264,5 +264,4 @@ Example usage: `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DENABLE_SDL=ON -
|
||||
### macOS
|
||||
| Flag | Description | Default |
|
||||
|---------------------|------------------------------------------------------|---------|
|
||||
| ENABLE_SWIFTUI_MACOS | Enable experimental native macOS SwiftUI GUI backend | OFF |
|
||||
| MACOS_BUNDLE | MacOS executable will be an application bundle | OFF |
|
||||
| MACOS_BUNDLE | macOS executable will be an application bundle | OFF |
|
||||
|
||||
@ -17,7 +17,7 @@ execute_process(
|
||||
OUTPUT_VARIABLE GIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
add_compile_definitions($<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:EMULATOR_HASH=${GIT_HASH}>)
|
||||
add_definitions(-DEMULATOR_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($<$<CONFIG:Debug>:CEMU_DEBUG_ASSERT>) # if build type is debug, set CEMU_DEBUG_ASSERT
|
||||
|
||||
add_compile_definitions($<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR}>)
|
||||
add_compile_definitions($<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR}>)
|
||||
add_compile_definitions($<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}>)
|
||||
add_definitions(-DEMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR})
|
||||
add_definitions(-DEMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR})
|
||||
add_definitions(-DEMULATOR_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_compile_definitions($<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:ENABLE_METAL=1>)
|
||||
add_definitions(-DENABLE_METAL=1)
|
||||
endif()
|
||||
|
||||
if (ENABLE_DISCORD_RPC)
|
||||
@ -232,7 +232,7 @@ if (ENABLE_CUBEB)
|
||||
set_property(TARGET cubeb PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
add_library(cubeb::cubeb ALIAS cubeb)
|
||||
endif()
|
||||
add_compile_definitions($<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:HAS_CUBEB=1>)
|
||||
add_compile_definitions("HAS_CUBEB=1")
|
||||
endif()
|
||||
|
||||
add_subdirectory("dependencies/ih264d" EXCLUDE_FROM_ALL)
|
||||
|
||||
@ -32,13 +32,7 @@ elseif(UNIX)
|
||||
add_compile_options(-Wno-ambiguous-reversed-operator)
|
||||
endif()
|
||||
|
||||
add_compile_options(
|
||||
$<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:-Wno-multichar>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:-Wno-invalid-offsetof>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:-Wno-switch>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:-Wno-ignored-attributes>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX,OBJC,OBJCXX>:-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)
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
add_library(CemuGui INTERFACE)
|
||||
target_include_directories(CemuGui INTERFACE "interface")
|
||||
|
||||
if(APPLE AND ENABLE_SWIFTUI_MACOS)
|
||||
add_subdirectory(swiftui)
|
||||
target_link_libraries(CemuGui INTERFACE CemuSwiftUiGui)
|
||||
elseif(ENABLE_WXWIDGETS)
|
||||
if(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()
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
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$<$<CONFIG:Debug>: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()
|
||||
@ -1,237 +0,0 @@
|
||||
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 }
|
||||
}
|
||||
@ -1,389 +0,0 @@
|
||||
#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 <Cocoa/Cocoa.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
|
||||
extern "C" void *CemuCreateSwiftUIRootViewController(void);
|
||||
|
||||
@interface CemuAppDelegate : NSObject <NSApplicationDelegate>
|
||||
- (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<WindowSystem::ErrorCategory> /*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<int32_t>(frame.size.width);
|
||||
g_window_info.height = static_cast<int32_t>(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; }
|
||||
Loading…
Reference in New Issue
Block a user