imgui-frontend-engineer: add iOS platform scaffolding
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
42
src/app/gfx/backend/metal_renderer.h
Normal file
42
src/app/gfx/backend/metal_renderer.h
Normal 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
|
||||
243
src/app/gfx/backend/metal_renderer.mm
Normal file
243
src/app/gfx/backend/metal_renderer.mm
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
35
src/app/platform/ios/ios_host.h
Normal file
35
src/app/platform/ios/ios_host.h
Normal 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
|
||||
61
src/app/platform/ios/ios_host.mm
Normal file
61
src/app/platform/ios/ios_host.mm
Normal 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
|
||||
22
src/app/platform/ios/ios_platform_state.h
Normal file
22
src/app/platform/ios/ios_platform_state.h
Normal 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
|
||||
30
src/app/platform/ios/ios_platform_state.mm
Normal file
30
src/app/platform/ios/ios_platform_state.mm
Normal 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
|
||||
56
src/app/platform/ios/ios_window_backend.h
Normal file
56
src/app/platform/ios/ios_window_backend.h
Normal 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
|
||||
351
src/app/platform/ios/ios_window_backend.mm
Normal file
351
src/app/platform/ios/ios_window_backend.mm
Normal 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
|
||||
@@ -272,6 +272,7 @@ class IWindowBackend {
|
||||
enum class WindowBackendType {
|
||||
SDL2,
|
||||
SDL3,
|
||||
IOS,
|
||||
Auto // Automatically select based on availability
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user