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

@@ -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;
}