backend-infra-engineer: Release v0.3.2 snapshot
This commit is contained in:
64
src/app/platform/app_delegate.h
Normal file
64
src/app/platform/app_delegate.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#ifndef YAZE_APP_PLATFORM_APP_DELEGATE_H
|
||||
#define YAZE_APP_PLATFORM_APP_DELEGATE_H
|
||||
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
/* Apple OSX and iOS (Darwin). */
|
||||
#import <CoreText/CoreText.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
|
||||
/* iOS in Xcode simulator */
|
||||
#import <PencilKit/PencilKit.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate,
|
||||
UIDocumentPickerDelegate,
|
||||
UITabBarControllerDelegate,
|
||||
PKCanvasViewDelegate>
|
||||
@property(strong, nonatomic) UIWindow *window;
|
||||
|
||||
@property UIDocumentPickerViewController *documentPicker;
|
||||
@property(nonatomic, copy) void (^completionHandler)(NSString *selectedFile);
|
||||
- (void)PresentDocumentPickerWithCompletionHandler:
|
||||
(void (^)(NSString *selectedFile))completionHandler;
|
||||
|
||||
// TODO: Setup a tab bar controller for multiple yaze instances
|
||||
@property(nonatomic) UITabBarController *tabBarController;
|
||||
|
||||
// TODO: Setup a font picker for the text editor and display settings
|
||||
@property(nonatomic) UIFontPickerViewController *fontPicker;
|
||||
|
||||
// TODO: Setup the pencil kit for drawing
|
||||
@property PKToolPicker *toolPicker;
|
||||
@property PKCanvasView *canvasView;
|
||||
|
||||
// TODO: Setup the file manager for file operations
|
||||
@property NSFileManager *fileManager;
|
||||
|
||||
@end
|
||||
|
||||
#elif TARGET_OS_MAC == 1
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialize the Cocoa application.
|
||||
*/
|
||||
void yaze_initialize_cocoa();
|
||||
|
||||
/**
|
||||
* @brief Run the Cocoa application delegate.
|
||||
*/
|
||||
int yaze_run_cocoa_app_delegate(const char *filename);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif // TARGET_OS_MAC
|
||||
|
||||
#endif // defined(__APPLE__) && defined(__MACH__)
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_APP_DELEGATE_H
|
||||
278
src/app/platform/app_delegate.mm
Normal file
278
src/app/platform/app_delegate.mm
Normal file
@@ -0,0 +1,278 @@
|
||||
// AppDelegate.mm
|
||||
|
||||
// Must define before any ImGui includes (needed by imgui_test_engine via editor_manager.h)
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
|
||||
#import "app/platform/app_delegate.h"
|
||||
#import "app/controller.h"
|
||||
#import "util/file_util.h"
|
||||
#import "app/editor/editor.h"
|
||||
#import "app/rom.h"
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
using std::span;
|
||||
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
/* Apple OSX and iOS (Darwin). */
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
|
||||
/* iOS in Xcode simulator */
|
||||
|
||||
#elif TARGET_OS_MAC == 1
|
||||
/* macOS */
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||
- (void)setupMenus;
|
||||
// - (void)changeApplicationIcon;
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
// - (void)changeApplicationIcon {
|
||||
// NSImage *newIcon = [NSImage imageNamed:@"newIcon"];
|
||||
// [NSApp setApplicationIconImage:newIcon];
|
||||
// }
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
[self setupMenus];
|
||||
|
||||
// Disable automatic UI state persistence to prevent crashes
|
||||
// macOS NSPersistentUIManager can crash if state gets corrupted
|
||||
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSQuitAlwaysKeepsWindows"];
|
||||
}
|
||||
|
||||
- (void)setupMenus {
|
||||
NSMenu *mainMenu = [NSApp mainMenu];
|
||||
|
||||
NSMenuItem *fileMenuItem = [mainMenu itemWithTitle:@"File"];
|
||||
if (!fileMenuItem) {
|
||||
NSMenu *fileMenu = [[NSMenu alloc] initWithTitle:@"File"];
|
||||
fileMenuItem = [[NSMenuItem alloc] initWithTitle:@"File" action:nil keyEquivalent:@""];
|
||||
[fileMenuItem setSubmenu:fileMenu];
|
||||
|
||||
NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:@"Open"
|
||||
action:@selector(openFileAction:)
|
||||
keyEquivalent:@"o"];
|
||||
[fileMenu addItem:openItem];
|
||||
|
||||
// Open Recent
|
||||
NSMenu *openRecentMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"];
|
||||
NSMenuItem *openRecentMenuItem = [[NSMenuItem alloc] initWithTitle:@"Open Recent"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[openRecentMenuItem setSubmenu:openRecentMenu];
|
||||
[fileMenu addItem:openRecentMenuItem];
|
||||
|
||||
// Add a separator
|
||||
[fileMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
// Save
|
||||
NSMenuItem *saveItem = [[NSMenuItem alloc] initWithTitle:@"Save" action:nil keyEquivalent:@"s"];
|
||||
[fileMenu addItem:saveItem];
|
||||
|
||||
// Separator
|
||||
[fileMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
// Options submenu
|
||||
NSMenu *optionsMenu = [[NSMenu alloc] initWithTitle:@"Options"];
|
||||
NSMenuItem *optionsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Options"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[optionsMenuItem setSubmenu:optionsMenu];
|
||||
|
||||
// Flag checkmark field
|
||||
NSMenuItem *flagItem = [[NSMenuItem alloc] initWithTitle:@"Flag"
|
||||
action:@selector(toggleFlagAction:)
|
||||
keyEquivalent:@""];
|
||||
[flagItem setTarget:self];
|
||||
[flagItem setState:NSControlStateValueOff];
|
||||
[optionsMenu addItem:flagItem];
|
||||
[fileMenu addItem:optionsMenuItem];
|
||||
|
||||
[mainMenu insertItem:fileMenuItem atIndex:1];
|
||||
}
|
||||
|
||||
NSMenuItem *editMenuItem = [mainMenu itemWithTitle:@"Edit"];
|
||||
if (!editMenuItem) {
|
||||
NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
|
||||
editMenuItem = [[NSMenuItem alloc] initWithTitle:@"Edit" action:nil keyEquivalent:@""];
|
||||
[editMenuItem setSubmenu:editMenu];
|
||||
|
||||
NSMenuItem *undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo" action:nil keyEquivalent:@"z"];
|
||||
|
||||
[editMenu addItem:undoItem];
|
||||
|
||||
NSMenuItem *redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo" action:nil keyEquivalent:@"Z"];
|
||||
|
||||
[editMenu addItem:redoItem];
|
||||
|
||||
// Add a separator
|
||||
[editMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
NSMenuItem *cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut"
|
||||
action:@selector(cutAction:)
|
||||
keyEquivalent:@"x"];
|
||||
[editMenu addItem:cutItem];
|
||||
|
||||
NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:nil keyEquivalent:@"c"];
|
||||
[editMenu addItem:copyItem];
|
||||
|
||||
NSMenuItem *pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste"
|
||||
action:nil
|
||||
keyEquivalent:@"v"];
|
||||
|
||||
[editMenu addItem:pasteItem];
|
||||
|
||||
// Add a separator
|
||||
[editMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
NSMenuItem *selectAllItem = [[NSMenuItem alloc] initWithTitle:@"Select All"
|
||||
action:nil
|
||||
keyEquivalent:@"a"];
|
||||
|
||||
[editMenu addItem:selectAllItem];
|
||||
|
||||
[mainMenu insertItem:editMenuItem atIndex:2];
|
||||
}
|
||||
|
||||
NSMenuItem *viewMenuItem = [mainMenu itemWithTitle:@"View"];
|
||||
if (!viewMenuItem) {
|
||||
NSMenu *viewMenu = [[NSMenu alloc] initWithTitle:@"View"];
|
||||
viewMenuItem = [[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""];
|
||||
[viewMenuItem setSubmenu:viewMenu];
|
||||
|
||||
// Emulator view button
|
||||
NSMenuItem *emulatorViewItem = [[NSMenuItem alloc] initWithTitle:@"Emulator View"
|
||||
action:nil
|
||||
keyEquivalent:@"1"];
|
||||
|
||||
[viewMenu addItem:emulatorViewItem];
|
||||
|
||||
// Hex Editor View
|
||||
NSMenuItem *hexEditorViewItem = [[NSMenuItem alloc] initWithTitle:@"Hex Editor View"
|
||||
action:nil
|
||||
keyEquivalent:@"2"];
|
||||
|
||||
[viewMenu addItem:hexEditorViewItem];
|
||||
|
||||
// Disassembly view button
|
||||
NSMenuItem *disassemblyViewItem = [[NSMenuItem alloc] initWithTitle:@"Disassembly View"
|
||||
action:nil
|
||||
keyEquivalent:@"3"];
|
||||
|
||||
[viewMenu addItem:disassemblyViewItem];
|
||||
|
||||
// Memory view button
|
||||
NSMenuItem *memoryViewItem = [[NSMenuItem alloc] initWithTitle:@"Memory View"
|
||||
action:nil
|
||||
keyEquivalent:@"4"];
|
||||
|
||||
[viewMenu addItem:memoryViewItem];
|
||||
|
||||
// Add a separator
|
||||
[viewMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
// Toggle fullscreen button
|
||||
NSMenuItem *toggleFullscreenItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Fullscreen"
|
||||
action:nil
|
||||
keyEquivalent:@"f"];
|
||||
|
||||
[viewMenu addItem:toggleFullscreenItem];
|
||||
|
||||
[mainMenu insertItem:viewMenuItem atIndex:3];
|
||||
}
|
||||
|
||||
NSMenuItem *helpMenuItem = [mainMenu itemWithTitle:@"Help"];
|
||||
if (!helpMenuItem) {
|
||||
NSMenu *helpMenu = [[NSMenu alloc] initWithTitle:@"Help"];
|
||||
helpMenuItem = [[NSMenuItem alloc] initWithTitle:@"Help" action:nil keyEquivalent:@""];
|
||||
[helpMenuItem setSubmenu:helpMenu];
|
||||
|
||||
// URL to online documentation
|
||||
NSMenuItem *documentationItem = [[NSMenuItem alloc] initWithTitle:@"Documentation"
|
||||
action:nil
|
||||
keyEquivalent:@"?"];
|
||||
[helpMenu addItem:documentationItem];
|
||||
|
||||
[mainMenu insertItem:helpMenuItem atIndex:4];
|
||||
}
|
||||
}
|
||||
|
||||
// Action method for the New menu item
|
||||
- (void)newFileAction:(id)sender {
|
||||
NSLog(@"New File action triggered");
|
||||
}
|
||||
|
||||
- (void)toggleFlagAction:(id)sender {
|
||||
NSMenuItem *flagItem = (NSMenuItem *)sender;
|
||||
if ([flagItem state] == NSControlStateValueOff) {
|
||||
[flagItem setState:NSControlStateValueOn];
|
||||
} else {
|
||||
[flagItem setState:NSControlStateValueOff];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)openFileAction:(id)sender {
|
||||
// TODO: Re-implmenent this without the SharedRom singleton
|
||||
// if (!yaze::SharedRom::shared_rom_
|
||||
// ->LoadFromFile(yaze::util::FileDialogWrapper::ShowOpenFileDialog())
|
||||
// .ok()) {
|
||||
// NSAlert *alert = [[NSAlert alloc] init];
|
||||
// [alert setMessageText:@"Error"];
|
||||
// [alert setInformativeText:@"Failed to load file."];
|
||||
// [alert addButtonWithTitle:@"OK"];
|
||||
// [alert runModal];
|
||||
// }
|
||||
}
|
||||
|
||||
- (void)cutAction:(id)sender {
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
- (void)openRecentFileAction:(id)sender {
|
||||
NSLog(@"Open Recent File action triggered");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
extern "C" void yaze_initialize_cococa() {
|
||||
@autoreleasepool {
|
||||
AppDelegate *delegate = [[AppDelegate alloc] init];
|
||||
[NSApplication sharedApplication];
|
||||
[NSApp setDelegate:delegate];
|
||||
[NSApp finishLaunching];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int yaze_run_cocoa_app_delegate(const char *filename) {
|
||||
yaze_initialize_cococa();
|
||||
auto controller = std::make_unique<yaze::Controller>();
|
||||
EXIT_IF_ERROR(controller->OnEntry(filename));
|
||||
while (controller->IsActive()) {
|
||||
@autoreleasepool {
|
||||
controller->OnInput();
|
||||
if (auto status = controller->OnLoad(); !status.ok()) {
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:@"Error"];
|
||||
[alert setInformativeText:[NSString stringWithUTF8String:status.message().data()]];
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
[alert runModal];
|
||||
break;
|
||||
}
|
||||
controller->DoRender();
|
||||
}
|
||||
}
|
||||
controller->OnExit();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
94
src/app/platform/asset_loader.cc
Normal file
94
src/app/platform/asset_loader.cc
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "app/platform/asset_loader.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "util/file_util.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
|
||||
std::vector<std::filesystem::path> AssetLoader::GetSearchPaths(const std::string& relative_path) {
|
||||
std::vector<std::filesystem::path> search_paths;
|
||||
|
||||
#ifdef __APPLE__
|
||||
// macOS bundle resource paths
|
||||
std::string bundle_root = yaze::util::GetBundleResourcePath();
|
||||
|
||||
// Try Contents/Resources first (standard bundle location)
|
||||
search_paths.push_back(std::filesystem::path(bundle_root) / "Contents" / "Resources" / relative_path);
|
||||
|
||||
// Try without Contents (if app is at root)
|
||||
search_paths.push_back(std::filesystem::path(bundle_root) / "Resources" / relative_path);
|
||||
|
||||
// Development paths (when running from build dir)
|
||||
search_paths.push_back(std::filesystem::path(bundle_root) / ".." / ".." / ".." / "assets" / relative_path);
|
||||
search_paths.push_back(std::filesystem::path(bundle_root) / ".." / ".." / ".." / ".." / "assets" / relative_path);
|
||||
#endif
|
||||
|
||||
// Standard relative paths (works for all platforms)
|
||||
search_paths.push_back(std::filesystem::path("assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../../assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../../../assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../../../../assets") / relative_path);
|
||||
|
||||
// Build directory paths
|
||||
search_paths.push_back(std::filesystem::path("build/assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../build/assets") / relative_path);
|
||||
|
||||
return search_paths;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::filesystem::path> AssetLoader::FindAssetFile(const std::string& relative_path) {
|
||||
auto search_paths = GetSearchPaths(relative_path);
|
||||
|
||||
for (const auto& path : search_paths) {
|
||||
if (std::filesystem::exists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Print searched paths
|
||||
std::string searched_paths;
|
||||
for (const auto& path : search_paths) {
|
||||
searched_paths += "\n - " + path.string();
|
||||
}
|
||||
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("Asset file not found: %s\nSearched paths:%s",
|
||||
relative_path, searched_paths));
|
||||
}
|
||||
|
||||
absl::StatusOr<std::string> AssetLoader::LoadTextFile(const std::string& relative_path) {
|
||||
auto path_result = FindAssetFile(relative_path);
|
||||
if (!path_result.ok()) {
|
||||
return path_result.status();
|
||||
}
|
||||
|
||||
const auto& path = *path_result;
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to open file: %s", path.string()));
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string content = buffer.str();
|
||||
|
||||
if (content.empty()) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("File is empty: %s", path.string()));
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
bool AssetLoader::AssetExists(const std::string& relative_path) {
|
||||
return FindAssetFile(relative_path).ok();
|
||||
}
|
||||
|
||||
|
||||
} // namespace yaze
|
||||
57
src/app/platform/asset_loader.h
Normal file
57
src/app/platform/asset_loader.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef YAZE_APP_PLATFORM_ASSET_LOADER_H_
|
||||
#define YAZE_APP_PLATFORM_ASSET_LOADER_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
|
||||
/**
|
||||
* @class AssetLoader
|
||||
* @brief Cross-platform asset file loading utility
|
||||
*
|
||||
* Handles platform-specific paths for loading assets from:
|
||||
* - macOS bundle resources
|
||||
* - Windows relative paths
|
||||
* - Linux relative paths
|
||||
* - Development build directories
|
||||
*/
|
||||
class AssetLoader {
|
||||
public:
|
||||
/**
|
||||
* Load a text file from the assets directory
|
||||
* @param relative_path Path relative to assets/ (e.g., "agent/system_prompt.txt")
|
||||
* @return File contents or error
|
||||
*/
|
||||
static absl::StatusOr<std::string> LoadTextFile(const std::string& relative_path);
|
||||
|
||||
/**
|
||||
* Find an asset file by trying multiple platform-specific paths
|
||||
* @param relative_path Path relative to assets/
|
||||
* @return Full path to file or error
|
||||
*/
|
||||
static absl::StatusOr<std::filesystem::path> FindAssetFile(const std::string& relative_path);
|
||||
|
||||
/**
|
||||
* Get list of search paths for a given asset
|
||||
* @param relative_path Path relative to assets/
|
||||
* @return Vector of paths to try in order
|
||||
*/
|
||||
static std::vector<std::filesystem::path> GetSearchPaths(const std::string& relative_path);
|
||||
|
||||
/**
|
||||
* Check if an asset file exists
|
||||
* @param relative_path Path relative to assets/
|
||||
* @return true if file exists in any search path
|
||||
*/
|
||||
static bool AssetExists(const std::string& relative_path);
|
||||
};
|
||||
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_ASSET_LOADER_H_
|
||||
303
src/app/platform/file_dialog.mm
Normal file
303
src/app/platform/file_dialog.mm
Normal file
@@ -0,0 +1,303 @@
|
||||
#include "util/file_util.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/features.h"
|
||||
|
||||
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
|
||||
#include <nfd.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
/* Apple OSX and iOS (Darwin). */
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
|
||||
/* iOS in Xcode simulator */
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
|
||||
#include "app/platform/app_delegate.h"
|
||||
|
||||
namespace {
|
||||
static std::string selectedFile;
|
||||
|
||||
void ShowOpenFileDialogImpl(void (^completionHandler)(std::string)) {
|
||||
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
|
||||
[appDelegate PresentDocumentPickerWithCompletionHandler:^(NSString *filePath) {
|
||||
selectedFile = std::string([filePath UTF8String]);
|
||||
completionHandler(selectedFile);
|
||||
}];
|
||||
}
|
||||
|
||||
std::string ShowOpenFileDialogSync() {
|
||||
__block std::string result;
|
||||
|
||||
ShowOpenFileDialogImpl(^(std::string filePath) {
|
||||
result = filePath;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() { return ShowOpenFileDialogSync(); }
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() { return ""; }
|
||||
|
||||
std::vector<std::string> yaze::util::FileDialogWrapper::GetFilesInFolder(
|
||||
const std::string &folder) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> yaze::util::FileDialogWrapper::GetSubdirectoriesInFolder(
|
||||
const std::string &folder) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string yaze::util::GetBundleResourcePath() {
|
||||
NSBundle* bundle = [NSBundle mainBundle];
|
||||
NSString* resourceDirectoryPath = [bundle bundlePath];
|
||||
NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
|
||||
return [path UTF8String];
|
||||
}
|
||||
|
||||
#elif TARGET_OS_MAC == 1
|
||||
/* macOS */
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogBespoke() {
|
||||
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
|
||||
[openPanel setCanChooseFiles:YES];
|
||||
[openPanel setCanChooseDirectories:NO];
|
||||
[openPanel setAllowsMultipleSelection:NO];
|
||||
|
||||
if ([openPanel runModal] == NSModalResponseOK) {
|
||||
NSURL* url = [[openPanel URLs] objectAtIndex:0];
|
||||
NSString* path = [url path];
|
||||
return std::string([path UTF8String]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
NSSavePanel* savePanel = [NSSavePanel savePanel];
|
||||
|
||||
if (!default_name.empty()) {
|
||||
[savePanel setNameFieldStringValue:[NSString stringWithUTF8String:default_name.c_str()]];
|
||||
}
|
||||
|
||||
if (!default_extension.empty()) {
|
||||
NSString* ext = [NSString stringWithUTF8String:default_extension.c_str()];
|
||||
[savePanel setAllowedFileTypes:@[ext]];
|
||||
}
|
||||
|
||||
if ([savePanel runModal] == NSModalResponseOK) {
|
||||
NSURL* url = [savePanel URL];
|
||||
NSString* path = [url path];
|
||||
return std::string([path UTF8String]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// Global feature flag-based dispatch methods
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowOpenFileDialogNFD();
|
||||
} else {
|
||||
return ShowOpenFileDialogBespoke();
|
||||
}
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowOpenFolderDialogNFD();
|
||||
} else {
|
||||
return ShowOpenFolderDialogBespoke();
|
||||
}
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowSaveFileDialogNFD(default_name, default_extension);
|
||||
} else {
|
||||
return ShowSaveFileDialogBespoke(default_name, default_extension);
|
||||
}
|
||||
}
|
||||
|
||||
// NFD implementation for macOS (fallback to bespoke if NFD not available)
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogNFD() {
|
||||
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
|
||||
NFD_Init();
|
||||
nfdu8char_t *out_path = NULL;
|
||||
nfdu8filteritem_t filters[1] = {{"Rom File", "sfc,smc"}};
|
||||
nfdopendialogu8args_t args = {0};
|
||||
args.filterList = filters;
|
||||
args.filterCount = 1;
|
||||
|
||||
nfdresult_t result = NFD_OpenDialogU8_With(&out_path, &args);
|
||||
if (result == NFD_OKAY) {
|
||||
std::string file_path(out_path);
|
||||
NFD_FreePath(out_path);
|
||||
NFD_Quit();
|
||||
return file_path;
|
||||
} else if (result == NFD_CANCEL) {
|
||||
NFD_Quit();
|
||||
return "";
|
||||
}
|
||||
NFD_Quit();
|
||||
return "";
|
||||
#else
|
||||
// NFD not compiled in, use bespoke
|
||||
return ShowOpenFileDialogBespoke();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialogNFD() {
|
||||
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
|
||||
NFD_Init();
|
||||
nfdu8char_t *out_path = NULL;
|
||||
nfdresult_t result = NFD_PickFolderU8(&out_path, NULL);
|
||||
|
||||
if (result == NFD_OKAY) {
|
||||
std::string folder_path(out_path);
|
||||
NFD_FreePath(out_path);
|
||||
NFD_Quit();
|
||||
return folder_path;
|
||||
} else if (result == NFD_CANCEL) {
|
||||
NFD_Quit();
|
||||
return "";
|
||||
}
|
||||
NFD_Quit();
|
||||
return "";
|
||||
#else
|
||||
// NFD not compiled in, use bespoke
|
||||
return ShowOpenFolderDialogBespoke();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogNFD(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
|
||||
NFD_Init();
|
||||
nfdu8char_t *out_path = NULL;
|
||||
|
||||
nfdsavedialogu8args_t args = {0};
|
||||
if (!default_extension.empty()) {
|
||||
// Create filter for the save dialog
|
||||
static nfdu8filteritem_t filters[3] = {
|
||||
{"Theme File", "theme"},
|
||||
{"Project File", "yaze"},
|
||||
{"ROM File", "sfc,smc"}
|
||||
};
|
||||
|
||||
if (default_extension == "theme") {
|
||||
args.filterList = &filters[0];
|
||||
args.filterCount = 1;
|
||||
} else if (default_extension == "yaze") {
|
||||
args.filterList = &filters[1];
|
||||
args.filterCount = 1;
|
||||
} else if (default_extension == "sfc" || default_extension == "smc") {
|
||||
args.filterList = &filters[2];
|
||||
args.filterCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!default_name.empty()) {
|
||||
args.defaultName = default_name.c_str();
|
||||
}
|
||||
|
||||
nfdresult_t result = NFD_SaveDialogU8_With(&out_path, &args);
|
||||
if (result == NFD_OKAY) {
|
||||
std::string file_path(out_path);
|
||||
NFD_FreePath(out_path);
|
||||
NFD_Quit();
|
||||
return file_path;
|
||||
} else if (result == NFD_CANCEL) {
|
||||
NFD_Quit();
|
||||
return "";
|
||||
}
|
||||
NFD_Quit();
|
||||
return "";
|
||||
#else
|
||||
// NFD not compiled in, use bespoke
|
||||
return ShowSaveFileDialogBespoke(default_name, default_extension);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialogBespoke() {
|
||||
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
|
||||
[openPanel setCanChooseFiles:NO];
|
||||
[openPanel setCanChooseDirectories:YES];
|
||||
[openPanel setAllowsMultipleSelection:NO];
|
||||
|
||||
if ([openPanel runModal] == NSModalResponseOK) {
|
||||
NSURL* url = [[openPanel URLs] objectAtIndex:0];
|
||||
NSString* path = [url path];
|
||||
return std::string([path UTF8String]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<std::string> yaze::util::FileDialogWrapper::GetFilesInFolder(
|
||||
const std::string& folder) {
|
||||
std::vector<std::string> filenames;
|
||||
NSFileManager* fileManager = [NSFileManager defaultManager];
|
||||
NSDirectoryEnumerator* enumerator =
|
||||
[fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
|
||||
NSString* file;
|
||||
while (file = [enumerator nextObject]) {
|
||||
if ([file hasPrefix:@"."]) {
|
||||
continue;
|
||||
}
|
||||
filenames.push_back(std::string([file UTF8String]));
|
||||
}
|
||||
return filenames;
|
||||
}
|
||||
|
||||
std::vector<std::string> yaze::util::FileDialogWrapper::GetSubdirectoriesInFolder(
|
||||
const std::string& folder) {
|
||||
std::vector<std::string> subdirectories;
|
||||
NSFileManager* fileManager = [NSFileManager defaultManager];
|
||||
NSDirectoryEnumerator* enumerator =
|
||||
[fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
|
||||
NSString* file;
|
||||
while (file = [enumerator nextObject]) {
|
||||
if ([file hasPrefix:@"."]) {
|
||||
continue;
|
||||
}
|
||||
BOOL isDirectory;
|
||||
NSString* path =
|
||||
[NSString stringWithFormat:@"%@/%@", [NSString stringWithUTF8String:folder.c_str()], file];
|
||||
[fileManager fileExistsAtPath:path isDirectory:&isDirectory];
|
||||
if (isDirectory) {
|
||||
subdirectories.push_back(std::string([file UTF8String]));
|
||||
}
|
||||
}
|
||||
return subdirectories;
|
||||
}
|
||||
|
||||
std::string yaze::util::GetBundleResourcePath() {
|
||||
NSBundle* bundle = [NSBundle mainBundle];
|
||||
NSString* resourceDirectoryPath = [bundle bundlePath];
|
||||
NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
|
||||
return [path UTF8String];
|
||||
}
|
||||
|
||||
#else
|
||||
// Unsupported platform
|
||||
#endif // TARGET_OS_MAC
|
||||
|
||||
#endif // __APPLE__ && __MACH__
|
||||
123
src/app/platform/file_dialog_nfd.cc
Normal file
123
src/app/platform/file_dialog_nfd.cc
Normal file
@@ -0,0 +1,123 @@
|
||||
// Windows and Linux implementation of FileDialogWrapper using nativefiledialog-extended
|
||||
#include "util/file_util.h"
|
||||
|
||||
#include <nfd.h>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace yaze {
|
||||
namespace util {
|
||||
|
||||
std::string FileDialogWrapper::ShowOpenFileDialog() {
|
||||
nfdchar_t* outPath = nullptr;
|
||||
nfdfilteritem_t filterItem[2] = {{"ROM Files", "sfc,smc"}, {"All Files", "*"}};
|
||||
nfdresult_t result = NFD_OpenDialog(&outPath, filterItem, 2, nullptr);
|
||||
|
||||
if (result == NFD_OKAY) {
|
||||
std::string path(outPath);
|
||||
NFD_FreePath(outPath);
|
||||
return path;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string FileDialogWrapper::ShowOpenFolderDialog() {
|
||||
nfdchar_t* outPath = nullptr;
|
||||
nfdresult_t result = NFD_PickFolder(&outPath, nullptr);
|
||||
|
||||
if (result == NFD_OKAY) {
|
||||
std::string path(outPath);
|
||||
NFD_FreePath(outPath);
|
||||
return path;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
nfdchar_t* outPath = nullptr;
|
||||
nfdfilteritem_t filterItem[1] = {{default_extension.empty() ? "All Files" : default_extension.c_str(),
|
||||
default_extension.empty() ? "*" : default_extension.c_str()}};
|
||||
|
||||
nfdresult_t result = NFD_SaveDialog(&outPath,
|
||||
default_extension.empty() ? nullptr : filterItem,
|
||||
default_extension.empty() ? 0 : 1,
|
||||
nullptr,
|
||||
default_name.c_str());
|
||||
|
||||
if (result == NFD_OKAY) {
|
||||
std::string path(outPath);
|
||||
NFD_FreePath(outPath);
|
||||
return path;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<std::string> FileDialogWrapper::GetSubdirectoriesInFolder(
|
||||
const std::string& folder_path) {
|
||||
std::vector<std::string> subdirs;
|
||||
|
||||
try {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(folder_path)) {
|
||||
if (entry.is_directory()) {
|
||||
subdirs.push_back(entry.path().string());
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
// Return empty vector on error
|
||||
}
|
||||
|
||||
return subdirs;
|
||||
}
|
||||
|
||||
std::vector<std::string> FileDialogWrapper::GetFilesInFolder(
|
||||
const std::string& folder_path) {
|
||||
std::vector<std::string> files;
|
||||
|
||||
try {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(folder_path)) {
|
||||
if (entry.is_regular_file()) {
|
||||
files.push_back(entry.path().string());
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
// Return empty vector on error
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
// Delegate to main implementations
|
||||
std::string FileDialogWrapper::ShowOpenFileDialogNFD() {
|
||||
return ShowOpenFileDialog();
|
||||
}
|
||||
|
||||
std::string FileDialogWrapper::ShowOpenFileDialogBespoke() {
|
||||
return ShowOpenFileDialog();
|
||||
}
|
||||
|
||||
std::string FileDialogWrapper::ShowSaveFileDialogNFD(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
return ShowSaveFileDialog(default_name, default_extension);
|
||||
}
|
||||
|
||||
std::string FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
return ShowSaveFileDialog(default_name, default_extension);
|
||||
}
|
||||
|
||||
std::string FileDialogWrapper::ShowOpenFolderDialogNFD() {
|
||||
return ShowOpenFolderDialog();
|
||||
}
|
||||
|
||||
std::string FileDialogWrapper::ShowOpenFolderDialogBespoke() {
|
||||
return ShowOpenFolderDialog();
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace yaze
|
||||
|
||||
140
src/app/platform/font_loader.cc
Normal file
140
src/app/platform/font_loader.cc
Normal file
@@ -0,0 +1,140 @@
|
||||
#include "app/platform/font_loader.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "util/file_util.h"
|
||||
#include "app/gui/core/icons.h"
|
||||
#include "imgui/imgui.h"
|
||||
#include "util/macro.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
static const char* KARLA_REGULAR = "Karla-Regular.ttf";
|
||||
static const char* ROBOTO_MEDIUM = "Roboto-Medium.ttf";
|
||||
static const char* COUSINE_REGULAR = "Cousine-Regular.ttf";
|
||||
static const char* DROID_SANS = "DroidSans.ttf";
|
||||
static const char* NOTO_SANS_JP = "NotoSansJP.ttf";
|
||||
static const char* IBM_PLEX_JP = "IBMPlexSansJP-Bold.ttf";
|
||||
|
||||
static const float FONT_SIZE_DEFAULT = 16.0F;
|
||||
static const float FONT_SIZE_DROID_SANS = 18.0F;
|
||||
static const float ICON_FONT_SIZE = 18.0F;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string SetFontPath(const std::string& font_path) {
|
||||
#ifdef __APPLE__
|
||||
#if TARGET_OS_IOS == 1
|
||||
const std::string kBundlePath = util::GetBundleResourcePath();
|
||||
return kBundlePath + font_path;
|
||||
#else
|
||||
return absl::StrCat(util::GetBundleResourcePath(), "Contents/Resources/font/",
|
||||
font_path);
|
||||
#endif
|
||||
#else
|
||||
return absl::StrCat("assets/font/", font_path);
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status LoadFont(const FontConfig& font_config) {
|
||||
ImGuiIO& imgui_io = ImGui::GetIO();
|
||||
std::string actual_font_path = SetFontPath(font_config.font_path);
|
||||
// Check if the file exists with std library first, since ImGui IO will assert
|
||||
// if the file does not exist
|
||||
if (!std::filesystem::exists(actual_font_path)) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Font file %s does not exist", actual_font_path));
|
||||
}
|
||||
|
||||
if (!imgui_io.Fonts->AddFontFromFileTTF(actual_font_path.data(),
|
||||
font_config.font_size)) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to load font from %s", actual_font_path));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status AddIconFont(const FontConfig& /*config*/) {
|
||||
static const ImWchar icons_ranges[] = {ICON_MIN_MD, 0xf900, 0};
|
||||
ImFontConfig icons_config{};
|
||||
icons_config.MergeMode = true;
|
||||
icons_config.GlyphOffset.y = 5.0F;
|
||||
icons_config.GlyphMinAdvanceX = 13.0F;
|
||||
icons_config.PixelSnapH = true;
|
||||
std::string icon_font_path = SetFontPath(FONT_ICON_FILE_NAME_MD);
|
||||
ImGuiIO& imgui_io = ImGui::GetIO();
|
||||
if (!imgui_io.Fonts->AddFontFromFileTTF(icon_font_path.c_str(), ICON_FONT_SIZE,
|
||||
&icons_config, icons_ranges)) {
|
||||
return absl::InternalError("Failed to add icon fonts");
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status AddJapaneseFont(const FontConfig& /*config*/) {
|
||||
ImFontConfig japanese_font_config{};
|
||||
japanese_font_config.MergeMode = true;
|
||||
japanese_font_config.GlyphOffset.y = 5.0F;
|
||||
japanese_font_config.GlyphMinAdvanceX = 13.0F;
|
||||
japanese_font_config.PixelSnapH = true;
|
||||
std::string japanese_font_path = SetFontPath(NOTO_SANS_JP);
|
||||
ImGuiIO& imgui_io = ImGui::GetIO();
|
||||
if (!imgui_io.Fonts->AddFontFromFileTTF(japanese_font_path.data(), ICON_FONT_SIZE,
|
||||
&japanese_font_config,
|
||||
imgui_io.Fonts->GetGlyphRangesJapanese())) {
|
||||
return absl::InternalError("Failed to add Japanese fonts");
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
absl::Status LoadPackageFonts() {
|
||||
if (font_registry.fonts.empty()) {
|
||||
// Initialize the font names and sizes with proper ImFontConfig initialization
|
||||
font_registry.fonts = {
|
||||
FontConfig{KARLA_REGULAR, FONT_SIZE_DEFAULT, {}, {}},
|
||||
FontConfig{ROBOTO_MEDIUM, FONT_SIZE_DEFAULT, {}, {}},
|
||||
FontConfig{COUSINE_REGULAR, FONT_SIZE_DEFAULT, {}, {}},
|
||||
FontConfig{IBM_PLEX_JP, FONT_SIZE_DEFAULT, {}, {}},
|
||||
FontConfig{DROID_SANS, FONT_SIZE_DROID_SANS, {}, {}},
|
||||
};
|
||||
}
|
||||
|
||||
// Load fonts with associated icon and Japanese merges
|
||||
for (const auto& font_config : font_registry.fonts) {
|
||||
RETURN_IF_ERROR(LoadFont(font_config));
|
||||
RETURN_IF_ERROR(AddIconFont(font_config));
|
||||
RETURN_IF_ERROR(AddJapaneseFont(font_config));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ReloadPackageFont(const FontConfig& config) {
|
||||
ImGuiIO& imgui_io = ImGui::GetIO();
|
||||
std::string actual_font_path = SetFontPath(config.font_path);
|
||||
if (!imgui_io.Fonts->AddFontFromFileTTF(actual_font_path.data(),
|
||||
config.font_size)) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to load font from %s", actual_font_path));
|
||||
}
|
||||
RETURN_IF_ERROR(AddIconFont(config));
|
||||
RETURN_IF_ERROR(AddJapaneseFont(config));
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
void LoadSystemFonts() {
|
||||
// Load Linux System Fonts into ImGui
|
||||
// System font loading is now handled by NFD (Native File Dialog)
|
||||
// This function is kept for compatibility but does nothing
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace yaze
|
||||
34
src/app/platform/font_loader.h
Normal file
34
src/app/platform/font_loader.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef YAZE_APP_PLATFORM_FONTLOADER_H
|
||||
#define YAZE_APP_PLATFORM_FONTLOADER_H
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
|
||||
struct FontConfig {
|
||||
const char* font_path;
|
||||
float font_size;
|
||||
ImFontConfig im_font_config;
|
||||
ImFontConfig jp_conf_config;
|
||||
};
|
||||
|
||||
struct FontState {
|
||||
std::vector<FontConfig> fonts;
|
||||
};
|
||||
|
||||
static FontState font_registry;
|
||||
|
||||
absl::Status LoadPackageFonts();
|
||||
|
||||
absl::Status ReloadPackageFont(const FontConfig& config);
|
||||
|
||||
void LoadSystemFonts();
|
||||
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_FONTLOADER_H
|
||||
59
src/app/platform/font_loader.mm
Normal file
59
src/app/platform/font_loader.mm
Normal file
@@ -0,0 +1,59 @@
|
||||
#include "app/platform/font_loader.h"
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#include "app/gui/core/icons.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
#if TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1
|
||||
/* iOS */
|
||||
void yaze::LoadSystemFonts() {}
|
||||
|
||||
#elif TARGET_OS_MAC == 1
|
||||
/* macOS */
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
void yaze::LoadSystemFonts() {
|
||||
NSArray *fontNames = @[ @"Helvetica", @"Times New Roman", @"Courier", @"Arial", @"Verdana" ];
|
||||
|
||||
for (NSString *fontName in fontNames) {
|
||||
NSFont *font = [NSFont fontWithName:fontName size:14.0];
|
||||
if (!font) {
|
||||
NSLog(@"Font not found: %@", fontName);
|
||||
continue;
|
||||
}
|
||||
|
||||
CTFontDescriptorRef fontDescriptor =
|
||||
CTFontDescriptorCreateWithNameAndSize((CFStringRef)font.fontName, font.pointSize);
|
||||
CFURLRef fontURL = (CFURLRef)CTFontDescriptorCopyAttribute(fontDescriptor, kCTFontURLAttribute);
|
||||
NSString *fontPath = [(NSURL *)fontURL path];
|
||||
CFRelease(fontDescriptor);
|
||||
|
||||
if (fontPath != nil && [[NSFileManager defaultManager] isReadableFileAtPath:fontPath]) {
|
||||
// Load the font into ImGui
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImFontConfig icons_config;
|
||||
icons_config.MergeMode = true;
|
||||
icons_config.GlyphOffset.y = 5.0f;
|
||||
icons_config.GlyphMinAdvanceX = 13.0f;
|
||||
icons_config.PixelSnapH = true;
|
||||
static const ImWchar icons_ranges[] = {ICON_MIN_MD, 0xf900, 0};
|
||||
static const float ICON_FONT_SIZE = 18.0f;
|
||||
ImFont *imFont = io.Fonts->AddFontFromFileTTF([fontPath UTF8String], 14.0f);
|
||||
if (!imFont) {
|
||||
NSLog(@"Failed to load font: %@", fontPath);
|
||||
}
|
||||
io.Fonts->AddFontFromFileTTF(FONT_ICON_FILE_NAME_MD, ICON_FONT_SIZE, &icons_config,
|
||||
icons_ranges);
|
||||
} else {
|
||||
NSLog(@"Font file not accessible: %@", fontPath);
|
||||
}
|
||||
|
||||
if (fontURL) {
|
||||
CFRelease(fontURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
117
src/app/platform/timing.h
Normal file
117
src/app/platform/timing.h
Normal file
@@ -0,0 +1,117 @@
|
||||
#ifndef YAZE_APP_CORE_TIMING_H
|
||||
#define YAZE_APP_CORE_TIMING_H
|
||||
|
||||
#include <SDL.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace yaze {
|
||||
|
||||
/**
|
||||
* @class TimingManager
|
||||
* @brief Provides accurate timing for animations and frame pacing
|
||||
*
|
||||
* This class solves the issue where ImGui::GetIO().DeltaTime only updates
|
||||
* when events are processed (mouse movement, etc). It uses SDL's performance
|
||||
* counter to provide accurate timing regardless of input events.
|
||||
*/
|
||||
class TimingManager {
|
||||
public:
|
||||
static TimingManager& Get() {
|
||||
static TimingManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update the timing manager (call once per frame)
|
||||
* @return The delta time since last update in seconds
|
||||
*/
|
||||
float Update() {
|
||||
uint64_t current_time = SDL_GetPerformanceCounter();
|
||||
float delta_time = 0.0f;
|
||||
|
||||
if (last_time_ > 0) {
|
||||
delta_time = (current_time - last_time_) / static_cast<float>(frequency_);
|
||||
|
||||
// Clamp delta time to prevent huge jumps (e.g., when debugging)
|
||||
if (delta_time > 0.1f) {
|
||||
delta_time = 0.1f;
|
||||
}
|
||||
|
||||
accumulated_time_ += delta_time;
|
||||
frame_count_++;
|
||||
|
||||
// Update FPS counter once per second
|
||||
if (accumulated_time_ >= 1.0f) {
|
||||
fps_ = static_cast<float>(frame_count_) / accumulated_time_;
|
||||
frame_count_ = 0;
|
||||
accumulated_time_ = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
last_time_ = current_time;
|
||||
last_delta_time_ = delta_time;
|
||||
return delta_time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the last frame's delta time in seconds
|
||||
*/
|
||||
float GetDeltaTime() const {
|
||||
return last_delta_time_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current FPS
|
||||
*/
|
||||
float GetFPS() const {
|
||||
return fps_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get total elapsed time since first update
|
||||
*/
|
||||
float GetElapsedTime() const {
|
||||
if (last_time_ == 0) return 0.0f;
|
||||
uint64_t current_time = SDL_GetPerformanceCounter();
|
||||
return (current_time - first_time_) / static_cast<float>(frequency_);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset the timing state
|
||||
*/
|
||||
void Reset() {
|
||||
last_time_ = 0;
|
||||
first_time_ = SDL_GetPerformanceCounter();
|
||||
accumulated_time_ = 0.0f;
|
||||
frame_count_ = 0;
|
||||
fps_ = 0.0f;
|
||||
last_delta_time_ = 0.0f;
|
||||
}
|
||||
|
||||
private:
|
||||
TimingManager() {
|
||||
frequency_ = SDL_GetPerformanceFrequency();
|
||||
first_time_ = SDL_GetPerformanceCounter();
|
||||
last_time_ = 0;
|
||||
accumulated_time_ = 0.0f;
|
||||
frame_count_ = 0;
|
||||
fps_ = 0.0f;
|
||||
last_delta_time_ = 0.0f;
|
||||
}
|
||||
|
||||
uint64_t frequency_;
|
||||
uint64_t first_time_;
|
||||
uint64_t last_time_;
|
||||
float accumulated_time_;
|
||||
uint32_t frame_count_;
|
||||
float fps_;
|
||||
float last_delta_time_;
|
||||
|
||||
TimingManager(const TimingManager&) = delete;
|
||||
TimingManager& operator=(const TimingManager&) = delete;
|
||||
};
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_CORE_TIMING_H
|
||||
|
||||
35
src/app/platform/view_controller.h
Normal file
35
src/app/platform/view_controller.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef YAZE_APP_CORE_PLATFORM_VIEW_CONTROLLER_H
|
||||
#define YAZE_APP_CORE_PLATFORM_VIEW_CONTROLLER_H
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#ifdef __OBJC__
|
||||
@interface AppViewController : NSViewController <NSWindowDelegate>
|
||||
@property(nonatomic) yaze::Controller *controller;
|
||||
@end
|
||||
#endif
|
||||
#else
|
||||
#ifdef __OBJC__
|
||||
@interface AppViewController : UIViewController <MTKViewDelegate>
|
||||
@property(nonatomic) yaze::Controller *controller;
|
||||
@property(nonatomic) UIHoverGestureRecognizer *hoverGestureRecognizer;
|
||||
@property(nonatomic) UIPinchGestureRecognizer *pinchRecognizer;
|
||||
@property(nonatomic) UISwipeGestureRecognizer *swipeRecognizer;
|
||||
@property(nonatomic) UILongPressGestureRecognizer *longPressRecognizer;
|
||||
@end
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __OBJC__
|
||||
@interface AppViewController () <MTKViewDelegate>
|
||||
@property(nonatomic, readonly) MTKView *mtkView;
|
||||
@property(nonatomic, strong) id<MTLDevice> device;
|
||||
@property(nonatomic, strong) id<MTLCommandQueue> commandQueue;
|
||||
@end
|
||||
#endif
|
||||
|
||||
#endif // __APPLE__
|
||||
|
||||
#endif // YAZE_APP_CORE_PLATFORM_VIEW_CONTROLLER_H
|
||||
1
src/app/platform/win32/yaze.rc
Normal file
1
src/app/platform/win32/yaze.rc
Normal file
@@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON DISCARDABLE "yaze.ico"
|
||||
237
src/app/platform/window.cc
Normal file
237
src/app/platform/window.cc
Normal file
@@ -0,0 +1,237 @@
|
||||
#include "app/platform/window.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/platform/font_loader.h"
|
||||
#include "util/sdl_deleter.h"
|
||||
#include "util/log.h"
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gui/core/style.h"
|
||||
#include "imgui/backends/imgui_impl_sdl2.h"
|
||||
#include "imgui/backends/imgui_impl_sdlrenderer2.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace {
|
||||
// Custom ImGui assertion handler to prevent crashes
|
||||
void ImGuiAssertionHandler(const char* expr, const char* file, int line,
|
||||
const char* msg) {
|
||||
// Log the assertion instead of crashing
|
||||
LOG_ERROR("ImGui", "Assertion failed: %s\nFile: %s:%d\nMessage: %s",
|
||||
expr, file, line, msg ? msg : "");
|
||||
|
||||
// Try to recover by resetting ImGui state
|
||||
static int error_count = 0;
|
||||
error_count++;
|
||||
|
||||
if (error_count > 5) {
|
||||
LOG_ERROR("ImGui", "Too many assertions, resetting workspace settings...");
|
||||
|
||||
// Backup and reset imgui.ini
|
||||
try {
|
||||
if (std::filesystem::exists("imgui.ini")) {
|
||||
std::filesystem::copy("imgui.ini", "imgui.ini.backup",
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
std::filesystem::remove("imgui.ini");
|
||||
LOG_INFO("ImGui", "Workspace settings reset. Backup saved to imgui.ini.backup");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("ImGui", "Failed to reset workspace: %s", e.what());
|
||||
}
|
||||
|
||||
error_count = 0; // Reset counter
|
||||
}
|
||||
|
||||
// Don't abort - let the program continue
|
||||
// The assertion is logged and workspace can be reset if needed
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace yaze {
|
||||
namespace core {
|
||||
|
||||
// Global flag for window resize state (used by emulator to pause)
|
||||
bool g_window_is_resizing = false;
|
||||
|
||||
absl::Status CreateWindow(Window& window, gfx::IRenderer* renderer, int flags) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("SDL_Init: %s\n", SDL_GetError()));
|
||||
}
|
||||
|
||||
SDL_DisplayMode display_mode;
|
||||
SDL_GetCurrentDisplayMode(0, &display_mode);
|
||||
int screen_width = display_mode.w * 0.8;
|
||||
int screen_height = display_mode.h * 0.8;
|
||||
|
||||
window.window_ = std::unique_ptr<SDL_Window, util::SDL_Deleter>(
|
||||
SDL_CreateWindow("Yet Another Zelda3 Editor", SDL_WINDOWPOS_UNDEFINED,
|
||||
SDL_WINDOWPOS_UNDEFINED, screen_width, screen_height,
|
||||
flags),
|
||||
util::SDL_Deleter());
|
||||
if (window.window_ == nullptr) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("SDL_CreateWindow: %s\n", SDL_GetError()));
|
||||
}
|
||||
|
||||
// Only initialize renderer if one is provided and not already initialized
|
||||
if (renderer && !renderer->GetBackendRenderer()) {
|
||||
if (!renderer->Initialize(window.window_.get())) {
|
||||
return absl::InternalError("Failed to initialize renderer");
|
||||
}
|
||||
}
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
|
||||
// Set custom assertion handler to prevent crashes
|
||||
#ifdef IMGUI_DISABLE_DEFAULT_ASSERT_HANDLER
|
||||
ImGui::SetAssertHandler(ImGuiAssertionHandler);
|
||||
#else
|
||||
// For release builds, assertions are already disabled
|
||||
LOG_INFO("Window", "ImGui assertions are disabled in this build");
|
||||
#endif
|
||||
|
||||
// Initialize ImGuiTestEngine after ImGui context is created
|
||||
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
test::TestManager::Get().InitializeUITesting();
|
||||
#endif
|
||||
|
||||
// Initialize ImGui backends if renderer is provided
|
||||
if (renderer) {
|
||||
SDL_Renderer* sdl_renderer = static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
|
||||
ImGui_ImplSDL2_InitForSDLRenderer(window.window_.get(), sdl_renderer);
|
||||
ImGui_ImplSDLRenderer2_Init(sdl_renderer);
|
||||
}
|
||||
|
||||
RETURN_IF_ERROR(LoadPackageFonts());
|
||||
|
||||
// Apply original YAZE colors as fallback, then try to load theme system
|
||||
gui::ColorsYaze();
|
||||
|
||||
// Audio is now handled by IAudioBackend in Emulator class
|
||||
// Keep legacy buffer allocation for backwards compatibility
|
||||
if (window.audio_device_ == 0) {
|
||||
const int audio_frequency = 48000;
|
||||
const size_t buffer_size = (audio_frequency / 50) * 2; // 1920 int16_t for stereo PAL
|
||||
|
||||
// CRITICAL FIX: Allocate buffer as ARRAY, not single value
|
||||
// Use new[] with shared_ptr custom deleter for proper array allocation
|
||||
window.audio_buffer_ = std::shared_ptr<int16_t>(
|
||||
new int16_t[buffer_size],
|
||||
std::default_delete<int16_t[]>());
|
||||
|
||||
// Note: Actual audio device is created by Emulator's IAudioBackend
|
||||
// This maintains compatibility with existing code paths
|
||||
LOG_INFO("Window", "Audio buffer allocated: %zu int16_t samples (backend in Emulator)",
|
||||
buffer_size);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ShutdownWindow(Window& window) {
|
||||
SDL_PauseAudioDevice(window.audio_device_, 1);
|
||||
SDL_CloseAudioDevice(window.audio_device_);
|
||||
|
||||
// Stop test engine WHILE ImGui context is still valid
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
test::TestManager::Get().StopUITesting();
|
||||
#endif
|
||||
|
||||
// TODO: BAD FIX, SLOW SHUTDOWN TAKES TOO LONG NOW
|
||||
// CRITICAL FIX: Shutdown graphics arena FIRST
|
||||
// This ensures all textures are destroyed while renderer is still valid
|
||||
LOG_INFO("Window", "Shutting down graphics arena...");
|
||||
gfx::Arena::Get().Shutdown();
|
||||
|
||||
// Shutdown ImGui implementations (after Arena but before context)
|
||||
LOG_INFO("Window", "Shutting down ImGui implementations...");
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui_ImplSDLRenderer2_Shutdown();
|
||||
|
||||
// Destroy ImGui context
|
||||
LOG_INFO("Window", "Destroying ImGui context...");
|
||||
ImGui::DestroyContext();
|
||||
|
||||
// NOW destroy test engine context (after ImGui context is destroyed)
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
test::TestManager::Get().DestroyUITestingContext();
|
||||
#endif
|
||||
|
||||
// Finally destroy window
|
||||
LOG_INFO("Window", "Destroying window...");
|
||||
SDL_DestroyWindow(window.window_.get());
|
||||
|
||||
LOG_INFO("Window", "Shutting down SDL...");
|
||||
SDL_Quit();
|
||||
|
||||
LOG_INFO("Window", "Shutdown complete");
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HandleEvents(Window& window) {
|
||||
SDL_Event event;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Protect SDL_PollEvent from crashing the app
|
||||
// macOS NSPersistentUIManager corruption can crash during event polling
|
||||
while (SDL_PollEvent(&event)) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
switch (event.type) {
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP: {
|
||||
io.KeyShift = ((SDL_GetModState() & KMOD_SHIFT) != 0);
|
||||
io.KeyCtrl = ((SDL_GetModState() & KMOD_CTRL) != 0);
|
||||
io.KeyAlt = ((SDL_GetModState() & KMOD_ALT) != 0);
|
||||
io.KeySuper = ((SDL_GetModState() & KMOD_GUI) != 0);
|
||||
break;
|
||||
}
|
||||
case SDL_WINDOWEVENT:
|
||||
switch (event.window.event) {
|
||||
case SDL_WINDOWEVENT_CLOSE:
|
||||
window.active_ = false;
|
||||
break;
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
case SDL_WINDOWEVENT_RESIZED:
|
||||
// Update display size for both resize and size_changed events
|
||||
io.DisplaySize.x = static_cast<float>(event.window.data1);
|
||||
io.DisplaySize.y = static_cast<float>(event.window.data2);
|
||||
core::g_window_is_resizing = true;
|
||||
break;
|
||||
case SDL_WINDOWEVENT_MINIMIZED:
|
||||
case SDL_WINDOWEVENT_HIDDEN:
|
||||
// Window is minimized/hidden
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
case SDL_WINDOWEVENT_RESTORED:
|
||||
case SDL_WINDOWEVENT_SHOWN:
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
// Window is restored - clear resize flag
|
||||
g_window_is_resizing = false;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
int mouseX;
|
||||
int mouseY;
|
||||
const int buttons = SDL_GetMouseState(&mouseX, &mouseY);
|
||||
|
||||
io.DeltaTime = 1.0f / 60.0f;
|
||||
io.MousePos = ImVec2(static_cast<float>(mouseX), static_cast<float>(mouseY));
|
||||
io.MouseDown[0] = buttons & SDL_BUTTON(SDL_BUTTON_LEFT);
|
||||
io.MouseDown[1] = buttons & SDL_BUTTON(SDL_BUTTON_RIGHT);
|
||||
io.MouseDown[2] = buttons & SDL_BUTTON(SDL_BUTTON_MIDDLE);
|
||||
|
||||
int wheel = 0;
|
||||
io.MouseWheel = static_cast<float>(wheel);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
} // namespace yaze
|
||||
33
src/app/platform/window.h
Normal file
33
src/app/platform/window.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#ifndef YAZE_CORE_WINDOW_H_
|
||||
#define YAZE_CORE_WINDOW_H_
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "util/sdl_deleter.h"
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/backend/irenderer.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace core {
|
||||
|
||||
struct Window {
|
||||
std::shared_ptr<SDL_Window> window_;
|
||||
SDL_AudioDeviceID audio_device_;
|
||||
std::shared_ptr<int16_t> audio_buffer_;
|
||||
bool active_ = true;
|
||||
};
|
||||
|
||||
// Legacy CreateWindow (deprecated - use Controller::OnEntry instead)
|
||||
// Kept for backward compatibility with test code
|
||||
absl::Status CreateWindow(Window& window, gfx::IRenderer* renderer = nullptr,
|
||||
int flags = SDL_WINDOW_RESIZABLE);
|
||||
absl::Status HandleEvents(Window &window);
|
||||
absl::Status ShutdownWindow(Window &window);
|
||||
|
||||
} // namespace core
|
||||
} // namespace yaze
|
||||
#endif // YAZE_CORE_WINDOW_H_
|
||||
Reference in New Issue
Block a user