imgui-frontend-engineer: add iOS platform scaffolding

This commit is contained in:
scawful
2025-12-28 10:57:53 -06:00
parent 954c612f69
commit c9df4372d7
48 changed files with 2328 additions and 304 deletions

View File

@@ -32,7 +32,7 @@ if(EMSCRIPTEN)
list(APPEND YAZE_APP_EXECUTABLE_SRC web/debug/yaze_debug_inspector.cc)
endif()
if (APPLE)
if (YAZE_PLATFORM_MACOS)
list(APPEND YAZE_APP_EXECUTABLE_SRC app/platform/app_delegate.mm)
add_executable(yaze MACOSX_BUNDLE ${YAZE_APP_EXECUTABLE_SRC} ${YAZE_RESOURCE_FILES})
@@ -79,7 +79,9 @@ endif()
target_link_libraries(yaze PRIVATE
yaze_editor
yaze_emulator
yaze_emulator_ui
yaze_agent
yaze_grpc_support
absl::failure_signal_handler
absl::flags
absl::flags_parse
@@ -103,7 +105,7 @@ if(WIN32)
endif()
endif()
if(APPLE)
if(YAZE_PLATFORM_MACOS)
target_link_libraries(yaze PUBLIC "-framework Cocoa")
endif()

View File

@@ -19,6 +19,13 @@ set(
app/platform/window_backend_factory.cc
)
if(YAZE_PLATFORM_IOS)
list(APPEND YAZE_APP_CORE_SRC
app/application.cc
app/controller.cc
)
endif()
# Window backend: SDL2 or SDL3 (mutually exclusive)
if(YAZE_USE_SDL3)
list(APPEND YAZE_APP_CORE_SRC
@@ -91,6 +98,9 @@ if(APPLE)
set(YAZE_APPLE_OBJCXX_SRC
app/platform/file_dialog.mm
app/platform/font_loader.mm
app/platform/ios/ios_host.mm
app/platform/ios/ios_platform_state.mm
app/platform/ios/ios_window_backend.mm
)
add_library(yaze_app_objcxx OBJECT ${YAZE_APPLE_OBJCXX_SRC})
@@ -116,13 +126,19 @@ if(APPLE)
endif()
target_link_libraries(yaze_app_objcxx PUBLIC ${ABSL_TARGETS} yaze_util ${YAZE_SDL2_TARGETS})
target_compile_definitions(yaze_app_objcxx PUBLIC MACOS)
find_library(COCOA_LIBRARY Cocoa)
if(NOT COCOA_LIBRARY)
message(FATAL_ERROR "Cocoa not found")
if(YAZE_PLATFORM_MACOS)
target_compile_definitions(yaze_app_objcxx PUBLIC MACOS)
elseif(YAZE_PLATFORM_IOS)
target_compile_definitions(yaze_app_objcxx PUBLIC YAZE_IOS)
endif()
if(YAZE_PLATFORM_MACOS)
find_library(COCOA_LIBRARY Cocoa)
if(NOT COCOA_LIBRARY)
message(FATAL_ERROR "Cocoa not found")
endif()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework ServiceManagement -framework Foundation -framework Cocoa")
endif()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework ServiceManagement -framework Foundation -framework Cocoa")
endif()
# Create the application core library
@@ -176,14 +192,14 @@ endif()
# gRPC Services (Optional)
if(YAZE_WITH_GRPC)
target_compile_definitions(yaze_app_core_lib PRIVATE YAZE_WITH_JSON)
# Link to consolidated gRPC support library
target_link_libraries(yaze_app_core_lib PUBLIC yaze_grpc_support)
# Note: Linking to yaze_grpc_support is moved to executable level to avoid cycle:
# yaze_grpc_support -> yaze_emulator -> yaze_app_core_lib -> yaze_grpc_support
message(STATUS " - gRPC ROM service + canvas automation enabled")
endif()
# Platform-specific libraries
if(APPLE)
if(YAZE_PLATFORM_MACOS)
target_link_libraries(yaze_app_core_lib PUBLIC ${COCOA_LIBRARY})
endif()
@@ -196,8 +212,10 @@ set_target_properties(yaze_app_core_lib PROPERTIES
# Platform-specific compile definitions
if(UNIX AND NOT APPLE)
target_compile_definitions(yaze_app_core_lib PRIVATE linux stricmp=strcasecmp)
elseif(APPLE)
elseif(YAZE_PLATFORM_MACOS)
target_compile_definitions(yaze_app_core_lib PRIVATE MACOS)
elseif(YAZE_PLATFORM_IOS)
target_compile_definitions(yaze_app_core_lib PRIVATE YAZE_IOS)
elseif(WIN32)
target_compile_definitions(yaze_app_core_lib PRIVATE WINDOWS)
endif()

View File

@@ -293,8 +293,10 @@ set_target_properties(yaze_editor PROPERTIES
# Platform-specific compile definitions
if(UNIX AND NOT APPLE)
target_compile_definitions(yaze_editor PRIVATE linux stricmp=strcasecmp)
elseif(APPLE)
elseif(YAZE_PLATFORM_MACOS)
target_compile_definitions(yaze_editor PRIVATE MACOS)
elseif(YAZE_PLATFORM_IOS)
target_compile_definitions(yaze_editor PRIVATE YAZE_IOS)
elseif(WIN32)
target_compile_definitions(yaze_editor PRIVATE WINDOWS)
endif()

View File

@@ -4,6 +4,10 @@
#include "app/platform/sdl_compat.h"
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include <algorithm>
#include <vector>
@@ -109,6 +113,9 @@ bool SDL2AudioBackend::Initialize(const AudioConfig& config) {
// but the hardware actually runs at 48000Hz, causing pitch/speed issues.
// SDL will handle internal resampling if the hardware doesn't support 48000Hz.
int allowed_changes = SDL_AUDIO_ALLOW_FORMAT_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE;
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
SDL_SetHint(SDL_HINT_AUDIO_CATEGORY, "ambient");
#endif
device_id_ = SDL_OpenAudioDevice(nullptr, 0, &want, &have, allowed_changes);
if (device_id_ == 0) {

View File

@@ -3,8 +3,8 @@
# because it's used by the main yaze app and test suites.
# This file only controls the STANDALONE emulator executable.
if(YAZE_BUILD_EMU AND NOT YAZE_MINIMAL_BUILD)
if(APPLE)
if(YAZE_BUILD_EMU AND NOT YAZE_MINIMAL_BUILD AND NOT YAZE_PLATFORM_IOS)
if(YAZE_PLATFORM_MACOS)
# Note: controller.cc is included here (not via library) because it depends on
# yaze_editor and yaze_gui. Including it in yaze_app_core_lib would create a cycle:
# yaze_agent -> yaze_app_core_lib -> yaze_editor -> yaze_agent
@@ -25,6 +25,7 @@ if(YAZE_BUILD_EMU AND NOT YAZE_MINIMAL_BUILD)
target_link_libraries(yaze_emu PRIVATE
yaze_editor
yaze_emulator
yaze_emulator_ui
yaze_agent
absl::flags
absl::flags_parse
@@ -68,6 +69,6 @@ if(YAZE_BUILD_EMU AND NOT YAZE_MINIMAL_BUILD)
message(STATUS "✓ yaze_emu_test: Headless emulator test harness configured")
message(STATUS "✓ yaze_emu: Standalone emulator executable configured")
else()
message(STATUS "○ Standalone emulator builds disabled (YAZE_BUILD_EMU=OFF or YAZE_MINIMAL_BUILD=ON)")
message(STATUS "○ Standalone emulator builds disabled (YAZE_BUILD_EMU=OFF, YAZE_MINIMAL_BUILD=ON, or iOS)")
message(STATUS " Note: yaze_emulator library still available for main app and tests")
endif()

View File

@@ -36,7 +36,33 @@ endif()
target_link_libraries(yaze_emulator PUBLIC
yaze_util
yaze_common
${ABSL_TARGETS}
${SDL_TARGETS}
)
if(YAZE_ENABLE_JSON AND TARGET nlohmann_json::nlohmann_json)
target_link_libraries(yaze_emulator PUBLIC nlohmann_json::nlohmann_json)
endif()
# Emulator UI library (depends on app core and GUI)
add_library(yaze_emulator_ui STATIC ${YAZE_EMU_GUI_SRC})
target_precompile_headers(yaze_emulator_ui PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/src/yaze_pch.h>"
)
target_include_directories(yaze_emulator_ui PUBLIC
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/app
${CMAKE_SOURCE_DIR}/incl
${SDL2_INCLUDE_DIR}
${PROJECT_BINARY_DIR}
)
target_link_libraries(yaze_emulator_ui PUBLIC
yaze_emulator
yaze_app_core_lib
yaze_gui
${ABSL_TARGETS}
${SDL_TARGETS}
)
@@ -47,11 +73,19 @@ set_target_properties(yaze_emulator PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
)
set_target_properties(yaze_emulator_ui PROPERTIES
POSITION_INDEPENDENT_CODE ON
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
)
# Platform-specific compile definitions
if(UNIX AND NOT APPLE)
target_compile_definitions(yaze_emulator PRIVATE linux stricmp=strcasecmp)
elseif(APPLE)
elseif(YAZE_PLATFORM_MACOS)
target_compile_definitions(yaze_emulator PRIVATE MACOS)
elseif(YAZE_PLATFORM_IOS)
target_compile_definitions(yaze_emulator PRIVATE YAZE_IOS)
elseif(WIN32)
target_compile_definitions(yaze_emulator PRIVATE WINDOWS)
endif()

View File

@@ -11,6 +11,7 @@
#include "app/emu/snes.h"
#include "rom/rom.h"
#include "util/platform_paths.h"
namespace {
@@ -70,9 +71,11 @@ absl::Status SaveStateManager::Initialize() {
rom_checksum_ = CalculateRomChecksum();
printf("[StateManager] ROM checksum: 0x%08X\n", rom_checksum_);
// Use ~/.yaze/states/ directory for state files
if (const char* home = std::getenv("HOME")) {
state_directory_ = std::string(home) + "/.yaze/states";
// Use app data directory for state files (sandbox-safe on iOS).
auto state_dir_status =
yaze::util::PlatformPaths::GetAppDataSubdirectory("states");
if (state_dir_status.ok()) {
state_directory_ = state_dir_status->string();
} else {
state_directory_ = "./states";
}

View File

@@ -116,7 +116,8 @@ void RenderNavBar(Emulator* emu) {
// Load ROM button
if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load ROM",
ImVec2(110, kButtonHeight))) {
std::string rom_path = util::FileDialogWrapper::ShowOpenFileDialog();
std::string rom_path = util::FileDialogWrapper::ShowOpenFileDialog(
util::MakeRomFileDialogOptions(false));
if (!rom_path.empty()) {
// Check if it's a valid ROM file extension
std::string ext = util::GetFileExtension(rom_path);

View File

@@ -0,0 +1,42 @@
#pragma once
#include "app/gfx/backend/irenderer.h"
namespace yaze {
namespace gfx {
class MetalRenderer final : public IRenderer {
public:
MetalRenderer() = default;
~MetalRenderer() override;
bool Initialize(SDL_Window* window) override;
void Shutdown() override;
TextureHandle CreateTexture(int width, int height) override;
TextureHandle CreateTextureWithFormat(int width, int height, uint32_t format,
int access) override;
void UpdateTexture(TextureHandle texture, const Bitmap& bitmap) override;
void DestroyTexture(TextureHandle texture) override;
bool LockTexture(TextureHandle texture, SDL_Rect* rect, void** pixels,
int* pitch) override;
void UnlockTexture(TextureHandle texture) override;
void Clear() override;
void Present() override;
void RenderCopy(TextureHandle texture, const SDL_Rect* srcrect,
const SDL_Rect* dstrect) override;
void SetRenderTarget(TextureHandle texture) override;
void SetDrawColor(SDL_Color color) override;
void* GetBackendRenderer() override;
void SetMetalView(void* view);
private:
void* metal_view_ = nullptr;
void* command_queue_ = nullptr;
TextureHandle render_target_ = nullptr;
};
} // namespace gfx
} // namespace yaze

View File

@@ -0,0 +1,243 @@
#include "app/gfx/backend/metal_renderer.h"
#if defined(__APPLE__)
#include <TargetConditionals.h>
#import <CoreFoundation/CoreFoundation.h>
#endif
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
#import <Metal/Metal.h>
#import <MetalKit/MetalKit.h>
#endif
#include "app/gfx/core/bitmap.h"
#include "app/platform/sdl_compat.h"
#include "util/log.h"
#include "util/sdl_deleter.h"
#include <algorithm>
namespace yaze {
namespace gfx {
MetalRenderer::~MetalRenderer() {
Shutdown();
}
bool MetalRenderer::Initialize(SDL_Window* window) {
(void)window;
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (!metal_view_) {
LOG_WARN("MetalRenderer", "Metal view not attached");
return false;
}
auto* view = static_cast<MTKView*>(metal_view_);
id<MTLDevice> device = view.device;
if (!device) {
device = MTLCreateSystemDefaultDevice();
view.device = device;
}
if (!device) {
LOG_WARN("MetalRenderer", "Failed to create Metal device");
return false;
}
if (!command_queue_) {
id<MTLCommandQueue> queue = [device newCommandQueue];
command_queue_ = (__bridge_retained void*)queue;
}
return true;
#else
return false;
#endif
}
void MetalRenderer::Shutdown() {
if (command_queue_) {
CFRelease(command_queue_);
command_queue_ = nullptr;
}
render_target_ = nullptr;
}
TextureHandle MetalRenderer::CreateTexture(int width, int height) {
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (!metal_view_) {
return nullptr;
}
auto* view = static_cast<MTKView*>(metal_view_);
id<MTLDevice> device = view.device;
if (!device) {
device = MTLCreateSystemDefaultDevice();
view.device = device;
}
if (!device) {
return nullptr;
}
MTLTextureDescriptor* descriptor =
[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
width:width
height:height
mipmapped:NO];
descriptor.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget;
descriptor.storageMode = MTLStorageModeShared;
id<MTLTexture> texture = [device newTextureWithDescriptor:descriptor];
return texture ? (__bridge_retained void*)texture : nullptr;
#else
(void)width;
(void)height;
return nullptr;
#endif
}
TextureHandle MetalRenderer::CreateTextureWithFormat(int width, int height,
uint32_t format,
int access) {
(void)format;
(void)access;
return CreateTexture(width, height);
}
void MetalRenderer::UpdateTexture(TextureHandle texture, const Bitmap& bitmap) {
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (!texture) {
return;
}
SDL_Surface* surface = bitmap.surface();
if (!surface || !surface->pixels || surface->w <= 0 || surface->h <= 0) {
return;
}
auto converted_surface =
std::unique_ptr<SDL_Surface, util::SDL_Surface_Deleter>(
platform::ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA8888));
if (!converted_surface || !converted_surface->pixels) {
return;
}
id<MTLTexture> metal_texture = (__bridge id<MTLTexture>)texture;
MTLRegion region = {
{0, 0, 0},
{static_cast<NSUInteger>(converted_surface->w),
static_cast<NSUInteger>(converted_surface->h),
1}};
[metal_texture replaceRegion:region
mipmapLevel:0
withBytes:converted_surface->pixels
bytesPerRow:converted_surface->pitch];
#else
(void)texture;
(void)bitmap;
#endif
}
void MetalRenderer::DestroyTexture(TextureHandle texture) {
if (!texture) {
return;
}
if (render_target_ == texture) {
render_target_ = nullptr;
}
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
CFRelease(texture);
#else
(void)texture;
#endif
}
bool MetalRenderer::LockTexture(TextureHandle texture, SDL_Rect* rect,
void** pixels, int* pitch) {
(void)texture;
(void)rect;
(void)pixels;
(void)pitch;
return false;
}
void MetalRenderer::UnlockTexture(TextureHandle texture) {
(void)texture;
}
void MetalRenderer::Clear() {
}
void MetalRenderer::Present() {
}
void MetalRenderer::RenderCopy(TextureHandle texture, const SDL_Rect* srcrect,
const SDL_Rect* dstrect) {
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (!texture || !render_target_ || !command_queue_) {
return;
}
id<MTLTexture> source = (__bridge id<MTLTexture>)texture;
id<MTLTexture> dest = (__bridge id<MTLTexture>)render_target_;
int src_x = srcrect ? srcrect->x : 0;
int src_y = srcrect ? srcrect->y : 0;
int src_w = srcrect ? srcrect->w : static_cast<int>(source.width);
int src_h = srcrect ? srcrect->h : static_cast<int>(source.height);
int dst_x = dstrect ? dstrect->x : 0;
int dst_y = dstrect ? dstrect->y : 0;
src_w = std::min(src_w, static_cast<int>(source.width) - src_x);
src_h = std::min(src_h, static_cast<int>(source.height) - src_y);
if (src_w <= 0 || src_h <= 0) {
return;
}
MTLOrigin src_origin = {static_cast<NSUInteger>(src_x),
static_cast<NSUInteger>(src_y),
0};
MTLSize src_size = {static_cast<NSUInteger>(src_w),
static_cast<NSUInteger>(src_h),
1};
MTLOrigin dst_origin = {static_cast<NSUInteger>(dst_x),
static_cast<NSUInteger>(dst_y),
0};
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)command_queue_;
id<MTLCommandBuffer> command_buffer = [queue commandBuffer];
id<MTLBlitCommandEncoder> blit = [command_buffer blitCommandEncoder];
[blit copyFromTexture:source
sourceSlice:0
sourceLevel:0
sourceOrigin:src_origin
sourceSize:src_size
toTexture:dest
destinationSlice:0
destinationLevel:0
destinationOrigin:dst_origin];
[blit endEncoding];
[command_buffer commit];
[command_buffer waitUntilCompleted];
#else
(void)texture;
(void)srcrect;
(void)dstrect;
#endif
}
void MetalRenderer::SetRenderTarget(TextureHandle texture) {
render_target_ = texture;
}
void MetalRenderer::SetDrawColor(SDL_Color color) {
(void)color;
}
void* MetalRenderer::GetBackendRenderer() {
return command_queue_;
}
void MetalRenderer::SetMetalView(void* view) {
metal_view_ = view;
}
} // namespace gfx
} // namespace yaze

View File

@@ -3,6 +3,10 @@
#include <memory>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include "app/gfx/backend/irenderer.h"
#include "app/gfx/backend/sdl2_renderer.h"
@@ -10,6 +14,10 @@
#include "app/gfx/backend/sdl3_renderer.h"
#endif
#if defined(__APPLE__)
#include "app/gfx/backend/metal_renderer.h"
#endif
namespace yaze {
namespace gfx {
@@ -20,6 +28,7 @@ namespace gfx {
enum class RendererBackendType {
SDL2, ///< SDL2 renderer backend
SDL3, ///< SDL3 renderer backend
Metal, ///< Metal renderer backend (Apple platforms)
kDefault, ///< Use the default backend based on build configuration
kAutoDetect ///< Automatically select the best available backend
};
@@ -71,11 +80,20 @@ class RendererFactory {
return std::make_unique<SDL2Renderer>();
#endif
case RendererBackendType::Metal:
#if defined(__APPLE__)
return std::make_unique<MetalRenderer>();
#else
return nullptr;
#endif
case RendererBackendType::kDefault:
case RendererBackendType::kAutoDetect:
default:
// Use the default backend based on build configuration
#ifdef YAZE_USE_SDL3
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
return std::make_unique<MetalRenderer>();
#elif defined(YAZE_USE_SDL3)
return std::make_unique<SDL3Renderer>();
#else
return std::make_unique<SDL2Renderer>();
@@ -102,6 +120,13 @@ class RendererFactory {
return false;
#endif
case RendererBackendType::Metal:
#if defined(__APPLE__)
return true;
#else
return false;
#endif
case RendererBackendType::kDefault:
case RendererBackendType::kAutoDetect:
// Default/auto-detect is always available
@@ -124,6 +149,8 @@ class RendererFactory {
return "SDL2";
case RendererBackendType::SDL3:
return "SDL3";
case RendererBackendType::Metal:
return "Metal";
case RendererBackendType::kDefault:
return "Default";
case RendererBackendType::kAutoDetect:
@@ -139,7 +166,9 @@ class RendererFactory {
* @return The default backend type based on build configuration.
*/
static RendererBackendType GetDefaultBackendType() {
#ifdef YAZE_USE_SDL3
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
return RendererBackendType::Metal;
#elif defined(YAZE_USE_SDL3)
return RendererBackendType::SDL3;
#else
return RendererBackendType::SDL2;

View File

@@ -34,8 +34,10 @@ macro(configure_gfx_library name)
)
if(UNIX AND NOT APPLE)
target_compile_definitions(${name} PRIVATE linux stricmp=strcasecmp)
elseif(APPLE)
elseif(YAZE_PLATFORM_MACOS)
target_compile_definitions(${name} PRIVATE MACOS)
elseif(YAZE_PLATFORM_IOS)
target_compile_definitions(${name} PRIVATE YAZE_IOS)
elseif(WIN32)
target_compile_definitions(${name} PRIVATE WINDOWS)
endif()
@@ -65,6 +67,12 @@ else()
)
endif()
if(APPLE)
list(APPEND GFX_BACKEND_SRC
app/gfx/backend/metal_renderer.mm
)
endif()
# build_cleaner:auto-maintain
set(GFX_RESOURCE_SRC
app/gfx/resource/memory_pool.cc

View File

@@ -128,8 +128,10 @@ foreach(LIB ${GUI_SUB_LIBS})
)
if(UNIX AND NOT APPLE)
target_compile_definitions(${LIB} PRIVATE linux stricmp=strcasecmp)
elseif(APPLE)
elseif(YAZE_PLATFORM_MACOS)
target_compile_definitions(${LIB} PRIVATE MACOS)
elseif(YAZE_PLATFORM_IOS)
target_compile_definitions(${LIB} PRIVATE YAZE_IOS)
elseif(WIN32)
target_compile_definitions(${LIB} PRIVATE WINDOWS)
endif()

View File

@@ -3,6 +3,10 @@
#include <fstream>
#include <sstream>
#ifdef __APPLE__
#include <TargetConditionals.h>
#endif
#include "absl/strings/str_format.h"
#include "util/file_util.h"
@@ -16,6 +20,12 @@ std::vector<std::filesystem::path> AssetLoader::GetSearchPaths(
// macOS bundle resource paths
std::string bundle_root = yaze::util::GetBundleResourcePath();
#if TARGET_OS_IOS == 1
// iOS app bundle resources live at the root.
search_paths.push_back(std::filesystem::path(bundle_root) / "assets" /
relative_path);
search_paths.push_back(std::filesystem::path(bundle_root) / relative_path);
#else
// Try Contents/Resources first (standard bundle location)
search_paths.push_back(std::filesystem::path(bundle_root) / "Contents" /
"Resources" / relative_path);
@@ -29,6 +39,7 @@ std::vector<std::filesystem::path> AssetLoader::GetSearchPaths(
".." / "assets" / relative_path);
search_paths.push_back(std::filesystem::path(bundle_root) / ".." / ".." /
".." / ".." / "assets" / relative_path);
#endif
#endif
// Standard relative paths (works for all platforms)

View File

@@ -1,10 +1,13 @@
#include "util/file_util.h"
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include "core/features.h"
#include "util/platform_paths.h"
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
#include <nfd.h>
@@ -19,45 +22,276 @@
#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
/* iOS in Xcode simulator */
#import <dispatch/dispatch.h>
#import <UIKit/UIKit.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include "app/platform/app_delegate.h"
namespace {
static std::string selectedFile;
@interface AppDelegate : UIResponder <UIApplicationDelegate, UIDocumentPickerDelegate>
@end
void ShowOpenFileDialogImpl(void (^completionHandler)(std::string)) {
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
[appDelegate PresentDocumentPickerWithCompletionHandler:^(NSString *filePath) {
selectedFile = std::string([filePath UTF8String]);
completionHandler(selectedFile);
}];
@interface AppDelegate (FileDialog)
- (void)PresentDocumentPickerWithCompletionHandler:
(void (^)(NSString *selectedFile))completionHandler
allowedTypes:(NSArray<UTType*> *)allowedTypes;
@end
namespace {
std::string TrimCopy(const std::string& input) {
const auto start = input.find_first_not_of(" \t\n\r");
if (start == std::string::npos) {
return "";
}
const auto end = input.find_last_not_of(" \t\n\r");
return input.substr(start, end - start + 1);
}
std::string ShowOpenFileDialogSync() {
__block std::string result;
std::vector<std::string> SplitFilterSpec(const std::string& spec) {
std::vector<std::string> tokens;
std::string current;
for (char ch : spec) {
if (ch == ',') {
std::string trimmed = TrimCopy(current);
if (!trimmed.empty() && trimmed[0] == '.') {
trimmed.erase(0, 1);
}
if (!trimmed.empty()) {
tokens.push_back(trimmed);
}
current.clear();
} else {
current.push_back(ch);
}
}
std::string trimmed = TrimCopy(current);
if (!trimmed.empty() && trimmed[0] == '.') {
trimmed.erase(0, 1);
}
if (!trimmed.empty()) {
tokens.push_back(trimmed);
}
return tokens;
}
ShowOpenFileDialogImpl(^(std::string filePath) {
result = filePath;
});
NSArray<UTType*>* BuildAllowedTypes(const yaze::util::FileDialogOptions& options) {
if (options.filters.empty()) {
return @[ UTTypeData ];
}
bool allow_all = false;
NSMutableArray<UTType*>* types = [NSMutableArray array];
for (const auto& filter : options.filters) {
const std::string spec = TrimCopy(filter.spec);
if (spec.empty() || spec == "*") {
allow_all = true;
continue;
}
for (const auto& token : SplitFilterSpec(spec)) {
if (token == "*") {
allow_all = true;
continue;
}
NSString* ext = [NSString stringWithUTF8String:token.c_str()];
UTType* type = [UTType typeWithFilenameExtension:ext];
if (!type) {
NSString* identifier = [NSString stringWithUTF8String:token.c_str()];
type = [UTType typeWithIdentifier:identifier];
}
if (type) {
[types addObject:type];
}
}
}
if (allow_all || [types count] == 0) {
return @[ UTTypeData ];
}
return types;
}
std::filesystem::path ResolveDocumentsPath() {
auto docs_result = yaze::util::PlatformPaths::GetUserDocumentsDirectory();
if (docs_result.ok()) {
return *docs_result;
}
auto temp_result = yaze::util::PlatformPaths::GetTempDirectory();
if (temp_result.ok()) {
return *temp_result;
}
std::error_code ec;
auto cwd = std::filesystem::current_path(ec);
if (!ec) {
return cwd;
}
return std::filesystem::path(".");
}
std::string NormalizeExtension(const std::string& ext) {
if (ext.empty()) {
return "";
}
if (ext.front() == '.') {
return ext;
}
return "." + ext;
}
std::string BuildSaveFilename(const std::string& default_name,
const std::string& default_extension) {
std::string name = default_name.empty() ? "yaze_output" : default_name;
const std::string normalized_ext = NormalizeExtension(default_extension);
if (!normalized_ext.empty()) {
auto dot_pos = name.find_last_of('.');
if (dot_pos == std::string::npos || dot_pos == 0) {
name += normalized_ext;
}
}
return name;
}
void ShowOpenFileDialogImpl(NSArray<UTType*>* allowed_types,
void (^completionHandler)(std::string)) {
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
if (!appDelegate) {
completionHandler("");
return;
}
[appDelegate PresentDocumentPickerWithCompletionHandler:^(NSString *filePath) {
completionHandler(std::string([filePath UTF8String]));
}
allowedTypes:allowed_types];
}
std::string ShowOpenFileDialogSync(
const yaze::util::FileDialogOptions& options) {
__block std::string result;
__block bool done = false;
NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
auto present_picker = ^{
ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
result = filePath;
done = true;
});
};
if ([NSThread isMainThread]) {
present_picker();
// Run a nested loop to keep UI responsive while waiting on selection.
while (!done) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
}
} else {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_main_queue(), ^{
ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
result = filePath;
dispatch_semaphore_signal(semaphore);
});
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
return result;
}
} // namespace
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() { return ShowOpenFileDialogSync(); }
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog(
const FileDialogOptions& options) {
return ShowOpenFileDialogSync(options);
}
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() { return ""; }
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() {
return ShowOpenFileDialog(FileDialogOptions{});
}
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogNFD() {
return ShowOpenFileDialog(FileDialogOptions{});
}
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogBespoke() {
return ShowOpenFileDialog(FileDialogOptions{});
}
void yaze::util::FileDialogWrapper::ShowOpenFileDialogAsync(
const FileDialogOptions& options,
std::function<void(const std::string&)> callback) {
if (!callback) {
return;
}
NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
auto callback_ptr =
std::make_shared<std::function<void(const std::string&)>>(
std::move(callback));
auto present_picker = ^{
ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
(*callback_ptr)(filePath);
});
};
if ([NSThread isMainThread]) {
present_picker();
} else {
dispatch_async(dispatch_get_main_queue(), present_picker);
}
}
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() {
return ResolveDocumentsPath().string();
}
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialog(
const std::string& default_name, const std::string& default_extension) {
const auto base_dir = ResolveDocumentsPath();
const std::string filename = BuildSaveFilename(default_name, default_extension);
return (base_dir / filename).string();
}
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogNFD(
const std::string& default_name, const std::string& default_extension) {
return ShowSaveFileDialog(default_name, default_extension);
}
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(
const std::string& default_name, const std::string& default_extension) {
return ShowSaveFileDialog(default_name, default_extension);
}
std::vector<std::string> yaze::util::FileDialogWrapper::GetFilesInFolder(
const std::string &folder) {
return {};
std::vector<std::string> files;
std::error_code ec;
for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
if (ec) {
break;
}
if (entry.is_regular_file()) {
files.push_back(entry.path().string());
}
}
return files;
}
std::vector<std::string> yaze::util::FileDialogWrapper::GetSubdirectoriesInFolder(
const std::string &folder) {
return {};
std::vector<std::string> directories;
std::error_code ec;
for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
if (ec) {
break;
}
if (entry.is_directory()) {
directories.push_back(entry.path().string());
}
}
return directories;
}
std::string yaze::util::GetBundleResourcePath() {
@@ -73,11 +307,91 @@ std::string yaze::util::GetBundleResourcePath() {
#import <Cocoa/Cocoa.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogBespoke() {
namespace {
std::string TrimCopy(const std::string& input) {
const auto start = input.find_first_not_of(" \t\n\r");
if (start == std::string::npos) {
return "";
}
const auto end = input.find_last_not_of(" \t\n\r");
return input.substr(start, end - start + 1);
}
std::vector<std::string> SplitFilterSpec(const std::string& spec) {
std::vector<std::string> tokens;
std::string current;
for (char ch : spec) {
if (ch == ',') {
std::string trimmed = TrimCopy(current);
if (!trimmed.empty() && trimmed[0] == '.') {
trimmed.erase(0, 1);
}
if (!trimmed.empty()) {
tokens.push_back(trimmed);
}
current.clear();
} else {
current.push_back(ch);
}
}
std::string trimmed = TrimCopy(current);
if (!trimmed.empty() && trimmed[0] == '.') {
trimmed.erase(0, 1);
}
if (!trimmed.empty()) {
tokens.push_back(trimmed);
}
return tokens;
}
std::vector<std::string> CollectExtensions(
const yaze::util::FileDialogOptions& options, bool* allow_all) {
std::vector<std::string> extensions;
if (!allow_all) {
return extensions;
}
*allow_all = false;
for (const auto& filter : options.filters) {
const std::string spec = TrimCopy(filter.spec);
if (spec.empty() || spec == "*") {
*allow_all = true;
continue;
}
for (const auto& token : SplitFilterSpec(spec)) {
if (token == "*") {
*allow_all = true;
} else {
extensions.push_back(token);
}
}
}
return extensions;
}
std::string ShowOpenFileDialogBespokeWithOptions(
const yaze::util::FileDialogOptions& options) {
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
[openPanel setCanChooseFiles:YES];
[openPanel setCanChooseDirectories:NO];
[openPanel setAllowsMultipleSelection:NO];
bool allow_all = false;
std::vector<std::string> extensions = CollectExtensions(options, &allow_all);
if (allow_all || extensions.empty()) {
[openPanel setAllowedFileTypes:nil];
} else {
NSMutableArray<NSString*>* allowed_types = [NSMutableArray array];
for (const auto& extension : extensions) {
NSString* ext = [NSString stringWithUTF8String:extension.c_str()];
if (ext) {
[allowed_types addObject:ext];
}
}
[openPanel setAllowedFileTypes:allowed_types];
}
if ([openPanel runModal] == NSModalResponseOK) {
NSURL* url = [[openPanel URLs] objectAtIndex:0];
@@ -88,6 +402,70 @@ std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogBespoke() {
return "";
}
std::string ShowOpenFileDialogNFDWithOptions(
const yaze::util::FileDialogOptions& options) {
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
NFD_Init();
nfdu8char_t* out_path = NULL;
const nfdu8filteritem_t* filter_list = nullptr;
size_t filter_count = 0;
std::vector<nfdu8filteritem_t> filter_items;
std::vector<std::string> filter_names;
std::vector<std::string> filter_specs;
if (!options.filters.empty()) {
filter_items.reserve(options.filters.size());
filter_names.reserve(options.filters.size());
filter_specs.reserve(options.filters.size());
for (const auto& filter : options.filters) {
std::string label = filter.label.empty() ? "Files" : filter.label;
std::string spec = filter.spec.empty() ? "*" : filter.spec;
filter_names.push_back(label);
filter_specs.push_back(spec);
filter_items.push_back(
{filter_names.back().c_str(), filter_specs.back().c_str()});
}
filter_list = filter_items.data();
filter_count = filter_items.size();
}
nfdopendialogu8args_t args = {0};
args.filterList = filter_list;
args.filterCount = filter_count;
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
return ShowOpenFileDialogBespokeWithOptions(options);
#endif
}
} // namespace
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogBespoke() {
return ShowOpenFileDialogBespokeWithOptions(FileDialogOptions{});
}
void yaze::util::FileDialogWrapper::ShowOpenFileDialogAsync(
const FileDialogOptions& options,
std::function<void(const std::string&)> callback) {
if (!callback) {
return;
}
callback(ShowOpenFileDialog(options));
}
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name,
const std::string& default_extension) {
NSSavePanel* savePanel = [NSSavePanel savePanel];
@@ -111,12 +489,16 @@ std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(const std::
}
// Global feature flag-based dispatch methods
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() {
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog(
const FileDialogOptions& options) {
if (core::FeatureFlags::get().kUseNativeFileDialog) {
return ShowOpenFileDialogNFD();
} else {
return ShowOpenFileDialogBespoke();
return ShowOpenFileDialogNFDWithOptions(options);
}
return ShowOpenFileDialogBespokeWithOptions(options);
}
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() {
return ShowOpenFileDialog(FileDialogOptions{});
}
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() {
@@ -138,30 +520,7 @@ std::string yaze::util::FileDialogWrapper::ShowSaveFileDialog(const std::string&
// 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
return ShowOpenFileDialogNFDWithOptions(FileDialogOptions{});
}
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialogNFD() {
@@ -300,4 +659,4 @@ std::string yaze::util::GetBundleResourcePath() {
// Unsupported platform
#endif // TARGET_OS_MAC
#endif // __APPLE__ && __MACH__
#endif // __APPLE__ && __MACH__

View File

@@ -11,11 +11,35 @@
namespace yaze {
namespace util {
std::string FileDialogWrapper::ShowOpenFileDialog() {
std::string FileDialogWrapper::ShowOpenFileDialog(
const FileDialogOptions& options) {
nfdchar_t* outPath = nullptr;
nfdfilteritem_t filterItem[2] = {{"ROM Files", "sfc,smc"},
{"All Files", "*"}};
nfdresult_t result = NFD_OpenDialog(&outPath, filterItem, 2, nullptr);
const nfdfilteritem_t* filter_list = nullptr;
size_t filter_count = 0;
std::vector<nfdfilteritem_t> filter_items;
std::vector<std::string> filter_names;
std::vector<std::string> filter_specs;
if (!options.filters.empty()) {
filter_items.reserve(options.filters.size());
filter_names.reserve(options.filters.size());
filter_specs.reserve(options.filters.size());
for (const auto& filter : options.filters) {
std::string label = filter.label.empty() ? "Files" : filter.label;
std::string spec = filter.spec.empty() ? "*" : filter.spec;
filter_names.push_back(label);
filter_specs.push_back(spec);
filter_items.push_back(
{filter_names.back().c_str(), filter_specs.back().c_str()});
}
filter_list = filter_items.data();
filter_count = filter_items.size();
}
nfdresult_t result =
NFD_OpenDialog(&outPath, filter_list, filter_count, nullptr);
if (result == NFD_OKAY) {
std::string path(outPath);
@@ -26,6 +50,19 @@ std::string FileDialogWrapper::ShowOpenFileDialog() {
return "";
}
std::string FileDialogWrapper::ShowOpenFileDialog() {
return ShowOpenFileDialog(FileDialogOptions{});
}
void FileDialogWrapper::ShowOpenFileDialogAsync(
const FileDialogOptions& options,
std::function<void(const std::string&)> callback) {
if (!callback) {
return;
}
callback(ShowOpenFileDialog(options));
}
std::string FileDialogWrapper::ShowOpenFolderDialog() {
nfdchar_t* outPath = nullptr;
nfdresult_t result = NFD_PickFolder(&outPath, nullptr);

View File

@@ -13,6 +13,12 @@ namespace util {
// Web implementation of FileDialogWrapper
// Triggers the existing file input element in the HTML
std::string FileDialogWrapper::ShowOpenFileDialog(
const FileDialogOptions& options) {
(void)options;
return ShowOpenFileDialog();
}
std::string FileDialogWrapper::ShowOpenFileDialog() {
#ifdef __EMSCRIPTEN__
// Trigger the existing file input element
@@ -33,6 +39,15 @@ std::string FileDialogWrapper::ShowOpenFileDialog() {
#endif
}
void FileDialogWrapper::ShowOpenFileDialogAsync(
const FileDialogOptions& options,
std::function<void(const std::string&)> callback) {
if (!callback) {
return;
}
callback(ShowOpenFileDialog(options));
}
std::string FileDialogWrapper::ShowOpenFolderDialog() {
// Folder picking not supported on web in the same way
return "";
@@ -57,4 +72,3 @@ std::vector<std::string> FileDialogWrapper::GetFilesInFolder(
} // namespace util
} // namespace yaze

View File

@@ -31,8 +31,17 @@ 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;
const std::string bundle_root = util::GetBundleResourcePath();
std::string bundle_path =
absl::StrCat(bundle_root, "assets/font/", font_path);
if (std::filesystem::exists(bundle_path)) {
return bundle_path;
}
bundle_path = absl::StrCat(bundle_root, font_path);
if (std::filesystem::exists(bundle_path)) {
return bundle_path;
}
return absl::StrCat("assets/font/", font_path);
#else
std::string bundle_path = absl::StrCat(
util::GetBundleResourcePath(), "Contents/Resources/font/", font_path);

View File

@@ -0,0 +1,35 @@
#pragma once
#include <string>
#include "absl/status/status.h"
#include "app/application.h"
namespace yaze {
namespace ios {
struct IOSHostConfig {
AppConfig app_config;
bool auto_start = true;
};
class IOSHost {
public:
IOSHost() = default;
~IOSHost();
absl::Status Initialize(const IOSHostConfig& config);
void Tick();
void Shutdown();
void SetMetalView(void* view);
void* GetMetalView() const;
private:
IOSHostConfig config_{};
void* metal_view_ = nullptr;
bool initialized_ = false;
};
} // namespace ios
} // namespace yaze

View File

@@ -0,0 +1,61 @@
#include "app/platform/ios/ios_host.h"
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
#import <MetalKit/MetalKit.h>
#endif
#include "app/platform/ios/ios_platform_state.h"
#include "util/log.h"
namespace yaze::ios {
IOSHost::~IOSHost() {
Shutdown();
}
absl::Status IOSHost::Initialize(const IOSHostConfig& config) {
config_ = config;
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (!metal_view_) {
return absl::FailedPreconditionError("Metal view not attached");
}
Application::Instance().Initialize(config_.app_config);
initialized_ = true;
LOG_INFO("IOSHost", "Initialized iOS host (stub)");
return absl::OkStatus();
#else
return absl::FailedPreconditionError("IOSHost only available on iOS");
#endif
}
void IOSHost::Tick() {
if (!initialized_) {
return;
}
Application::Instance().Tick();
}
void IOSHost::Shutdown() {
if (!initialized_) {
return;
}
Application::Instance().Shutdown();
initialized_ = false;
}
void IOSHost::SetMetalView(void* view) {
metal_view_ = view;
platform::ios::SetMetalView(view);
}
void* IOSHost::GetMetalView() const {
return metal_view_;
}
} // namespace yaze::ios

View File

@@ -0,0 +1,22 @@
#pragma once
namespace yaze {
namespace platform {
namespace ios {
struct SafeAreaInsets {
float left = 0.0f;
float right = 0.0f;
float top = 0.0f;
float bottom = 0.0f;
};
void SetMetalView(void* view);
void* GetMetalView();
void SetSafeAreaInsets(float left, float right, float top, float bottom);
SafeAreaInsets GetSafeAreaInsets();
} // namespace ios
} // namespace platform
} // namespace yaze

View File

@@ -0,0 +1,30 @@
#include "app/platform/ios/ios_platform_state.h"
namespace yaze {
namespace platform {
namespace ios {
namespace {
void* g_metal_view = nullptr;
SafeAreaInsets g_safe_area_insets = {};
} // namespace
void SetMetalView(void* view) {
g_metal_view = view;
}
void* GetMetalView() {
return g_metal_view;
}
void SetSafeAreaInsets(float left, float right, float top, float bottom) {
g_safe_area_insets = {left, right, top, bottom};
}
SafeAreaInsets GetSafeAreaInsets() {
return g_safe_area_insets;
}
} // namespace ios
} // namespace platform
} // namespace yaze

View File

@@ -0,0 +1,56 @@
#pragma once
#include <memory>
#include <string>
#include "absl/status/status.h"
#include "app/platform/iwindow.h"
namespace yaze {
namespace platform {
class IOSWindowBackend final : public IWindowBackend {
public:
IOSWindowBackend() = default;
~IOSWindowBackend() override = default;
absl::Status Initialize(const WindowConfig& config) override;
absl::Status Shutdown() override;
bool IsInitialized() const override;
bool PollEvent(WindowEvent& out_event) override;
void ProcessNativeEvent(void* native_event) override;
WindowStatus GetStatus() const override;
bool IsActive() const override;
void SetActive(bool active) override;
void GetSize(int* width, int* height) const override;
void SetSize(int width, int height) override;
std::string GetTitle() const override;
void SetTitle(const std::string& title) override;
bool InitializeRenderer(gfx::IRenderer* renderer) override;
SDL_Window* GetNativeWindow() override;
absl::Status InitializeImGui(gfx::IRenderer* renderer) override;
void ShutdownImGui() override;
void NewImGuiFrame() override;
void RenderImGui(gfx::IRenderer* renderer) override;
uint32_t GetAudioDevice() const override;
std::shared_ptr<int16_t> GetAudioBuffer() const override;
std::string GetBackendName() const override;
int GetSDLVersion() const override;
private:
bool initialized_ = false;
bool imgui_initialized_ = false;
WindowStatus status_{};
std::string title_;
void* metal_view_ = nullptr;
void* command_queue_ = nullptr;
};
} // namespace platform
} // namespace yaze

View File

@@ -0,0 +1,351 @@
#include "app/platform/ios/ios_window_backend.h"
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
#import <CoreFoundation/CoreFoundation.h>
#import <Metal/Metal.h>
#import <MetalKit/MetalKit.h>
#import <UIKit/UIKit.h>
#endif
#include <algorithm>
#include "app/gfx/backend/metal_renderer.h"
#include "app/gui/core/style.h"
#include "app/platform/font_loader.h"
#include "app/platform/ios/ios_platform_state.h"
#include "imgui/backends/imgui_impl_metal.h"
#include "imgui/imgui.h"
#include "util/log.h"
namespace yaze {
namespace platform {
namespace {
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
UIEdgeInsets GetSafeAreaInsets(MTKView* view) {
if (!view) {
return UIEdgeInsetsZero;
}
if (@available(iOS 11.0, *)) {
return view.safeAreaInsets;
}
return UIEdgeInsetsZero;
}
void ApplyTouchStyle(MTKView* view) {
ImGuiStyle& style = ImGui::GetStyle();
const float frame_height = ImGui::GetFrameHeight();
const float target_height = std::max(44.0f, frame_height);
const float touch_extra =
std::clamp((target_height - frame_height) * 0.5f, 0.0f, 16.0f);
style.TouchExtraPadding = ImVec2(touch_extra, touch_extra);
const float font_size = ImGui::GetFontSize();
if (font_size > 0.0f) {
style.ScrollbarSize = std::max(style.ScrollbarSize, font_size * 1.1f);
style.GrabMinSize = std::max(style.GrabMinSize, font_size * 0.9f);
style.FramePadding.x = std::max(style.FramePadding.x, font_size * 0.55f);
style.FramePadding.y = std::max(style.FramePadding.y, font_size * 0.35f);
style.ItemSpacing.x = std::max(style.ItemSpacing.x, font_size * 0.45f);
style.ItemSpacing.y = std::max(style.ItemSpacing.y, font_size * 0.35f);
}
const UIEdgeInsets insets = GetSafeAreaInsets(view);
const float safe_x = std::max(insets.left, insets.right);
const float safe_y = std::max(insets.top, insets.bottom);
style.DisplaySafeAreaPadding = ImVec2(safe_x, safe_y);
ios::SetSafeAreaInsets(insets.left, insets.right, insets.top,
insets.bottom);
}
#endif
} // namespace
absl::Status IOSWindowBackend::Initialize(const WindowConfig& config) {
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
metal_view_ = ios::GetMetalView();
if (!metal_view_) {
return absl::FailedPreconditionError("Metal view not set");
}
title_ = config.title;
status_.is_active = true;
status_.is_focused = true;
status_.is_fullscreen = config.fullscreen;
auto* view = static_cast<MTKView*>(metal_view_);
status_.width = static_cast<int>(view.bounds.size.width);
status_.height = static_cast<int>(view.bounds.size.height);
initialized_ = true;
return absl::OkStatus();
#else
(void)config;
return absl::FailedPreconditionError(
"IOSWindowBackend is only available on iOS");
#endif
}
absl::Status IOSWindowBackend::Shutdown() {
ShutdownImGui();
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (command_queue_) {
CFRelease(command_queue_);
command_queue_ = nullptr;
}
#endif
metal_view_ = nullptr;
initialized_ = false;
return absl::OkStatus();
}
bool IOSWindowBackend::IsInitialized() const {
return initialized_;
}
bool IOSWindowBackend::PollEvent(WindowEvent& out_event) {
out_event = WindowEvent{};
return false;
}
void IOSWindowBackend::ProcessNativeEvent(void* native_event) {
(void)native_event;
}
WindowStatus IOSWindowBackend::GetStatus() const {
WindowStatus status = status_;
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (metal_view_) {
auto* view = static_cast<MTKView*>(metal_view_);
status.width = static_cast<int>(view.bounds.size.width);
status.height = static_cast<int>(view.bounds.size.height);
}
#endif
return status;
}
bool IOSWindowBackend::IsActive() const {
return status_.is_active;
}
void IOSWindowBackend::SetActive(bool active) {
status_.is_active = active;
}
void IOSWindowBackend::GetSize(int* width, int* height) const {
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (metal_view_) {
auto* view = static_cast<MTKView*>(metal_view_);
if (width) {
*width = static_cast<int>(view.bounds.size.width);
}
if (height) {
*height = static_cast<int>(view.bounds.size.height);
}
return;
}
#endif
if (width) {
*width = 0;
}
if (height) {
*height = 0;
}
}
void IOSWindowBackend::SetSize(int width, int height) {
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (metal_view_) {
auto* view = static_cast<MTKView*>(metal_view_);
view.drawableSize = CGSizeMake(width, height);
}
#else
(void)width;
(void)height;
#endif
}
std::string IOSWindowBackend::GetTitle() const {
return title_;
}
void IOSWindowBackend::SetTitle(const std::string& title) {
title_ = title;
}
bool IOSWindowBackend::InitializeRenderer(gfx::IRenderer* renderer) {
if (!renderer || !metal_view_) {
return false;
}
if (renderer->GetBackendRenderer()) {
return true;
}
auto* metal_renderer = dynamic_cast<gfx::MetalRenderer*>(renderer);
if (metal_renderer) {
metal_renderer->SetMetalView(metal_view_);
} else {
LOG_WARN("IOSWindowBackend", "Non-Metal renderer selected on iOS");
}
return renderer->Initialize(nullptr);
}
SDL_Window* IOSWindowBackend::GetNativeWindow() {
return nullptr;
}
absl::Status IOSWindowBackend::InitializeImGui(gfx::IRenderer* renderer) {
if (imgui_initialized_) {
return absl::OkStatus();
}
if (!renderer) {
return absl::InvalidArgumentError("Renderer is null");
}
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
if (!metal_view_) {
return absl::FailedPreconditionError("Metal view not set");
}
auto* view = static_cast<MTKView*>(metal_view_);
id<MTLDevice> device = view.device;
if (!device) {
device = MTLCreateSystemDefaultDevice();
view.device = device;
}
if (!device) {
return absl::InternalError("Failed to create Metal device");
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
if (!ImGui_ImplMetal_Init(device)) {
return absl::InternalError("ImGui_ImplMetal_Init failed");
}
auto font_status = LoadPackageFonts();
if (!font_status.ok()) {
ImGui_ImplMetal_Shutdown();
ImGui::DestroyContext();
return font_status;
}
gui::ColorsYaze();
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
ApplyTouchStyle(view);
#endif
if (!command_queue_) {
id<MTLCommandQueue> queue = [device newCommandQueue];
command_queue_ = (__bridge_retained void*)queue;
}
imgui_initialized_ = true;
LOG_INFO("IOSWindowBackend", "ImGui initialized with Metal backend");
return absl::OkStatus();
#else
return absl::FailedPreconditionError(
"IOSWindowBackend is only available on iOS");
#endif
}
void IOSWindowBackend::ShutdownImGui() {
if (!imgui_initialized_) {
return;
}
ImGui_ImplMetal_Shutdown();
ImGui::DestroyContext();
imgui_initialized_ = false;
}
void IOSWindowBackend::NewImGuiFrame() {
if (!imgui_initialized_) {
return;
}
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
auto* view = static_cast<MTKView*>(metal_view_);
if (!view) {
return;
}
ApplyTouchStyle(view);
auto* render_pass = view.currentRenderPassDescriptor;
if (!render_pass) {
return;
}
ImGui_ImplMetal_NewFrame(render_pass);
#endif
}
void IOSWindowBackend::RenderImGui(gfx::IRenderer* renderer) {
if (!imgui_initialized_) {
return;
}
ImGui::Render();
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
(void)renderer;
auto* view = static_cast<MTKView*>(metal_view_);
if (!view || !view.currentDrawable) {
return;
}
auto* render_pass = view.currentRenderPassDescriptor;
if (!render_pass || !command_queue_) {
return;
}
id<MTLCommandQueue> queue =
(__bridge id<MTLCommandQueue>)command_queue_;
id<MTLCommandBuffer> command_buffer = [queue commandBuffer];
id<MTLRenderCommandEncoder> encoder =
[command_buffer renderCommandEncoderWithDescriptor:render_pass];
ImGui_ImplMetal_RenderDrawData(ImGui::GetDrawData(), command_buffer, encoder);
[encoder endEncoding];
[command_buffer presentDrawable:view.currentDrawable];
[command_buffer commit];
#else
(void)renderer;
#endif
}
uint32_t IOSWindowBackend::GetAudioDevice() const {
return 0;
}
std::shared_ptr<int16_t> IOSWindowBackend::GetAudioBuffer() const {
return nullptr;
}
std::string IOSWindowBackend::GetBackendName() const {
return "iOS-Metal";
}
int IOSWindowBackend::GetSDLVersion() const {
return 0;
}
} // namespace platform
} // namespace yaze

View File

@@ -272,6 +272,7 @@ class IWindowBackend {
enum class WindowBackendType {
SDL2,
SDL3,
IOS,
Auto // Automatically select based on availability
};

View File

@@ -12,7 +12,7 @@
#endif
#else
#ifdef __OBJC__
@interface AppViewController : UIViewController <MTKViewDelegate>
@interface AppViewController : UIViewController <MTKViewDelegate, UIGestureRecognizerDelegate>
@property(nonatomic) yaze::Controller *controller;
@property(nonatomic) UIHoverGestureRecognizer *hoverGestureRecognizer;
@property(nonatomic) UIPinchGestureRecognizer *pinchRecognizer;

View File

@@ -5,6 +5,14 @@
#include "app/platform/sdl2_window_backend.h"
#include "util/log.h"
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
#include "app/platform/ios/ios_window_backend.h"
#endif
#ifdef YAZE_USE_SDL3
#include "app/platform/sdl3_window_backend.h"
#endif
@@ -33,6 +41,15 @@ std::unique_ptr<IWindowBackend> WindowBackendFactory::Create(
return std::make_unique<SDL2WindowBackend>();
#endif
case WindowBackendType::IOS:
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
return std::make_unique<IOSWindowBackend>();
#else
LOG_WARN("WindowBackendFactory",
"iOS backend requested on non-iOS platform");
return nullptr;
#endif
case WindowBackendType::Auto:
default:
return Create(GetDefaultType());
@@ -40,6 +57,9 @@ std::unique_ptr<IWindowBackend> WindowBackendFactory::Create(
}
WindowBackendType WindowBackendFactory::GetDefaultType() {
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
return WindowBackendType::IOS;
#endif
#ifdef YAZE_USE_SDL3
return WindowBackendType::SDL3;
#else
@@ -66,6 +86,13 @@ bool WindowBackendFactory::IsAvailable(WindowBackendType type) {
case WindowBackendType::Auto:
return true; // Auto always available
case WindowBackendType::IOS:
#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
return true;
#else
return false;
#endif
default:
return false;
}