diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b66f5de44..c99133738b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -461,6 +461,14 @@ if (APPLE) ) endif () +if (IOS) + find_library(UIKIT_FRAMEWORK UIKit REQUIRED) + find_library(UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED) + target_sources(dusk PRIVATE src/dusk/ios/FileSelectDialog.m) + set_source_files_properties(src/dusk/ios/FileSelectDialog.m PROPERTIES COMPILE_FLAGS -fobjc-arc) + target_link_libraries(dusk PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK}) +endif () + include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake) aurora_copy_runtime_dlls(dusk) diff --git a/files.cmake b/files.cmake index e91415b3be..a65b7572c4 100644 --- a/files.cmake +++ b/files.cmake @@ -1342,6 +1342,8 @@ set(DUSK_FILES src/dusk/endian.cpp src/dusk/extras.c src/dusk/extras.cpp + src/dusk/file_select.cpp + src/dusk/file_select.hpp src/dusk/frame_interpolation.cpp src/dusk/globals.cpp src/dusk/gyro.cpp diff --git a/src/dusk/file_select.cpp b/src/dusk/file_select.cpp new file mode 100644 index 0000000000..fcda233c56 --- /dev/null +++ b/src/dusk/file_select.cpp @@ -0,0 +1,91 @@ +#include "file_select.hpp" + +#include + +#include +#include + +#if defined(__APPLE__) +#include +#endif + +#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#define USE_IOS_DIALOG 1 +#include "ios/FileSelectDialog.h" +#else +#define USE_IOS_DIALOG 0 +#endif + +namespace dusk { +namespace { + +#if USE_IOS_DIALOG +struct IOSDialogCallbackState { + FileCallback callback; + void* userdata; +}; + +void onIOSDialogFinished(void* userdata, const char* path, const char* error) { + std::unique_ptr state(static_cast(userdata)); + + if (error != nullptr) { + state->callback(state->userdata, nullptr, error); + return; + } + + if (path == nullptr) { + state->callback(state->userdata, nullptr, nullptr); + return; + } + + state->callback(state->userdata, path, nullptr); +} +#else +struct SDLDialogCallbackState { + FileCallback callback; + void* userdata; +}; + +void onSDLDialogFinished(void* userdata, const char* const* filelist, [[maybe_unused]] int filter) { + std::unique_ptr state(static_cast(userdata)); + + if (filelist == nullptr) { + state->callback(state->userdata, nullptr, SDL_GetError()); + return; + } + + if (filelist[0] == nullptr) { + state->callback(state->userdata, nullptr, nullptr); + return; + } + + state->callback(state->userdata, filelist[0], nullptr); +} +#endif + +} // namespace + +void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window, + const SDL_DialogFileFilter* filters, int nfilters, const char* default_location, + bool allow_many) { + if (callback == nullptr) { + return; + } + +#if USE_IOS_DIALOG + auto state = std::make_unique(); + state->callback = callback; + state->userdata = userdata; + + Dusk_iOS_ShowFileSelect(&onIOSDialogFinished, state.release(), window, filters, nfilters, + default_location, allow_many); +#else + auto state = std::make_unique(); + state->callback = callback; + state->userdata = userdata; + + SDL_ShowOpenFileDialog(&onSDLDialogFinished, state.release(), window, filters, nfilters, + default_location, allow_many); +#endif +} +} // namespace dusk diff --git a/src/dusk/file_select.hpp b/src/dusk/file_select.hpp new file mode 100644 index 0000000000..175c5aa086 --- /dev/null +++ b/src/dusk/file_select.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +struct SDL_Window; + +namespace dusk { + +using FileCallback = void (*)(void* userdata, const char* path, const char* error); + +void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window, + const SDL_DialogFileFilter* filters, int nfilters, const char* default_location, + bool allow_many); + +} // namespace dusk diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index eccf9b3c20..f6ed8b4c8e 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -3,23 +3,24 @@ #include #include #include -#include -#include "fmt/format.h" +#define IMGUI_DEFINE_MATH_OPERATORS #include "imgui.h" #include +#include "fmt/format.h" #include "ImGuiConsole.hpp" - #include "JSystem/JUtility/JUTGamePad.h" +#include "SDL3/SDL_events.h" #include "SDL3/SDL_mouse.h" -#include "m_Do/m_Do_controller_pad.h" -#include "m_Do/m_Do_main.h" +#include "aurora/lib/window.hpp" +#include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" +#include "dusk/dusk.h" #include "dusk/main.h" #include "dusk/settings.h" -#include "dusk/audio/DuskAudioSystem.h" -#include "dusk/dusk.h" +#include "m_Do/m_Do_controller_pad.h" +#include "m_Do/m_Do_main.h" #include "tracy/Tracy.hpp" #if _WIN32 @@ -30,6 +31,30 @@ using namespace std::string_literals; using namespace std::string_view_literals; +namespace { +ImVec2 TouchEventToScreenPos(const SDL_TouchFingerEvent& touch) { + const AuroraWindowSize size = aurora::window::get_window_size(); + return ImVec2{ + touch.x * static_cast(size.width), + touch.y * static_cast(size.height), + }; +} + +ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) { + while (window != nullptr) { + const bool canScrollX = window->ScrollMax.x > 0.0f; + const bool canScrollY = window->ScrollMax.y > 0.0f; + const bool canScrollWithMouse = (window->Flags & (ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoMouseInputs)) == 0; + if ((canScrollX || canScrollY) && canScrollWithMouse) { + return window; + } + window = window->ParentWindow; + } + return nullptr; +} +} // namespace + namespace dusk { float ImGuiScale() { return 1.0f; } @@ -208,6 +233,51 @@ namespace dusk { ImGuiConsole::ImGuiConsole() {} + void ImGuiConsole::HandleSDLEvent(const SDL_Event& event) { + if (!IsGameLaunched) { + return; + } + + switch (event.type) { + case SDL_EVENT_FINGER_DOWN: + if (!m_touchTapActive) { + m_touchTapActive = true; + m_touchTapMoved = false; + m_touchTapFingerId = event.tfinger.fingerID; + m_touchTapStartPos = TouchEventToScreenPos(event.tfinger); + } + break; + case SDL_EVENT_FINGER_MOTION: + if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) { + const auto currentPos = TouchEventToScreenPos(event.tfinger); + const auto delta = currentPos - m_touchTapStartPos; + if (ImLengthSqr(delta) > 144.0f) { + m_touchTapMoved = true; + } + } + break; + case SDL_EVENT_FINGER_UP: + if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) { + const bool shouldToggle = + !m_touchTapMoved && (m_isHidden || !ImGui::GetIO().WantCaptureMouse); + m_touchTapActive = false; + m_touchTapMoved = false; + if (shouldToggle) { + m_isHidden = !m_isHidden; + getSettings().backend.duskMenuOpen.setValue(!m_isHidden); + Save(); + } + } + break; + case SDL_EVENT_FINGER_CANCELED: + m_touchTapActive = false; + m_touchTapMoved = false; + break; + default: + break; + } + } + void ImGuiConsole::UpdateSettings() { getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && ImGui::IsKeyDown(ImGuiKey_Tab); @@ -237,26 +307,31 @@ namespace dusk { if (!dusk::IsGameLaunched) { m_preLaunchWindow.draw(); } - - m_isHidden = getSettings().backend.duskMenuOpen; - CheckMenuViewToggle(ImGuiKey_F1, m_isHidden); + m_isHidden = !getSettings().backend.duskMenuOpen; + bool showMenu = !dusk::IsGameLaunched || !CheckMenuViewToggle(ImGuiKey_F1, m_isHidden); if (dusk::IsGameLaunched) { - if (getSettings().backend.duskMenuOpen != m_isHidden) { - m_isHidden = !getSettings().backend.duskMenuOpen; - getSettings().backend.duskMenuOpen.setValue(m_isHidden); - config::Save(); + const bool menuOpen = !m_isHidden; + if (getSettings().backend.duskMenuOpen != menuOpen) { + getSettings().backend.duskMenuOpen.setValue(menuOpen); + Save(); } } - if ((!dusk::IsGameLaunched || getSettings().backend.duskMenuOpen) && ImGui::BeginMainMenuBar()) { + if (showMenu && ImGui::BeginMainMenuBar()) { m_menuGame.draw(); m_menuEnhancements.draw(); m_menuTools.draw(); - ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 100.0f * ImGuiScale()); - ImGuiIO& io = ImGui::GetIO(); - ImGuiStringViewText(fmt::format(FMT_STRING("FPS: {:.2f}\n"), io.Framerate)); + const auto fpsLabel = + fmt::format(FMT_STRING("FPS: {:.2f}\n"), ImGui::GetIO().Framerate); + const auto fpsSize = + ImGui::CalcTextSize(fpsLabel.data(), fpsLabel.data() + fpsLabel.size()); + ImGui::SetCursorPosX( + ImMax(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - + ImGui::GetStyle().DisplaySafeAreaPadding.x - + fpsSize.x - ImGui::GetStyle().ItemSpacing.x)); + ImGuiStringViewText(fpsLabel); ImGui::EndMainMenuBar(); } @@ -267,10 +342,15 @@ namespace dusk { } if (dusk::IsGameLaunched && !m_isLaunchInitialized) { - m_toasts.emplace_back("Press F1 to toggle menu"s, 2.5f); + m_toasts.emplace_back(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ? + "Tap to toggle menu"s : + "Press F1 to toggle menu"s, + 2.5f); m_isLaunchInitialized = true; } + UpdateDragScroll(); + m_menuGame.windowControllerConfig(); m_menuGame.windowInputViewer(); if (dusk::IsGameLaunched) { @@ -289,8 +369,7 @@ namespace dusk { DuskDebugPad(); // temporary, remove later // Only show cursor when menu or any windows are open - if ((!dusk::IsGameLaunched || getSettings().backend.duskMenuOpen) || ImGui::GetIO().MetricsRenderWindows > 0) - { + if (showMenu || ImGui::GetIO().MetricsRenderWindows > 0) { ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. } else { @@ -306,6 +385,57 @@ namespace dusk { ShowPipelineProgress(); } + void ImGuiConsole::UpdateDragScroll() { + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiIO& io = ImGui::GetIO(); + + if (io.MouseSource != ImGuiMouseSource_TouchScreen) { + m_dragScrollWindow = nullptr; + return; + } + + if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + m_dragScrollWindow = nullptr; + return; + } + + if (io.WantTextInput || (g.ActiveId != 0 && g.InputTextState.ID == g.ActiveId)) { + m_dragScrollWindow = nullptr; + return; + } + + if (!ImGui::IsMouseDragging(ImGuiMouseButton_Left, io.MouseDragThreshold)) { + return; + } + + if (m_dragScrollWindow == nullptr) { + ImGuiWindow* hoveredWindow = nullptr; + ImGuiWindow* hoveredWindowUnderMovingWindow = nullptr; + ImGui::FindHoveredWindowEx(io.MousePos, false, &hoveredWindow, + &hoveredWindowUnderMovingWindow); + m_dragScrollWindow = FindDragScrollWindow(hoveredWindow); + m_dragScrollLastMousePos = io.MousePos; + } + + if (m_dragScrollWindow == nullptr) { + return; + } + + const auto mouseDelta = io.MousePos - m_dragScrollLastMousePos; + m_dragScrollLastMousePos = io.MousePos; + + if (mouseDelta.x != 0.0f && m_dragScrollWindow->ScrollMax.x > 0.0f) { + ImGui::SetScrollX(m_dragScrollWindow, + ImClamp(m_dragScrollWindow->Scroll.x - mouseDelta.x, 0.0f, + m_dragScrollWindow->ScrollMax.x)); + } + if (mouseDelta.y != 0.0f && m_dragScrollWindow->ScrollMax.y > 0.0f) { + ImGui::SetScrollY(m_dragScrollWindow, + ImClamp(m_dragScrollWindow->Scroll.y - mouseDelta.y, 0.0f, + m_dragScrollWindow->ScrollMax.y)); + } + } + bool ImGuiConsole::CheckMenuViewToggle(ImGuiKey key, bool& active) { if (ImGui::IsKeyPressed(key)) { active = !active; diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 0296dc24cc..0670618cf2 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -1,11 +1,13 @@ #ifndef DUSK_IMGUI_HPP #define DUSK_IMGUI_HPP -#include #include #include #include +#include +#include + #include "ImGuiFirstRunPreset.hpp" #include "ImGuiMenuEnhancements.hpp" #include "ImGuiMenuGame.hpp" @@ -13,10 +15,14 @@ #include "ImGuiPreLaunchWindow.hpp" #include "imgui.h" +union SDL_Event; +struct ImGuiWindow; + namespace dusk { class ImGuiConsole { public: ImGuiConsole(); + void HandleSDLEvent(const SDL_Event& event); void UpdateSettings(); void PreDraw(); void PostDraw(); @@ -34,6 +40,12 @@ private: bool m_isHidden = true; bool m_isLaunchInitialized = false; + bool m_touchTapActive = false; + bool m_touchTapMoved = false; + SDL_FingerID m_touchTapFingerId = 0; + ImVec2 m_touchTapStartPos = {}; + ImGuiWindow* m_dragScrollWindow = nullptr; + ImVec2 m_dragScrollLastMousePos = {}; std::deque m_toasts; ImGuiFirstRunPreset m_firstRunPreset; @@ -46,6 +58,7 @@ private: void ShowToasts(); void ShowPipelineProgress(); + void UpdateDragScroll(); }; extern ImGuiConsole g_imguiConsole; diff --git a/src/dusk/imgui/ImGuiEngine.cpp b/src/dusk/imgui/ImGuiEngine.cpp index 0c0b5e934d..46bfa8f3ab 100644 --- a/src/dusk/imgui/ImGuiEngine.cpp +++ b/src/dusk/imgui/ImGuiEngine.cpp @@ -3,12 +3,14 @@ #include #include #include +#include #include #include #include #include #include +#include "aurora/lib/window.hpp" #include "dusk/logging.h" #ifdef IMGUI_ENABLE_FREETYPE @@ -37,6 +39,20 @@ ImTextureID AddTexture(const char* assetName) { } return aurora_imgui_add_texture(image.width, image.height, image.data.get()); } + +ImVec2 GetDisplaySafeAreaPadding() { + SDL_Window* window = aurora::window::get_sdl_window(); + if (window == nullptr) { + return ImVec2(0.0f, 0.0f); + } + + SDL_Rect safeRect{}; + if (!SDL_GetWindowSafeArea(window, &safeRect)) { + return ImVec2(0.0f, 0.0f); + } + + return ImVec2(static_cast(safeRect.x), static_cast(safeRect.y)); +} } // namespace ImFont* ImGuiEngine::fontNormal; @@ -75,6 +91,7 @@ void ImGuiEngine_Initialize(float scale) { ImGuiIO& io = ImGui::GetIO(); io.Fonts->Clear(); io.FontGlobalScale = scale > 0.0f ? 1.0f / scale : 1.0f; + io.ConfigWindowsMoveFromTitleBarOnly = IsMobile; ImGuiEngine::fontNormal = CreateFont(std::floor(18.f * scale), GetAssetPath("Inter-Regular.ttf"), "Inter Regular"); @@ -104,6 +121,7 @@ void ImGuiEngine_Initialize(float scale) { style.PopupRounding = 7.0; style.TabBorderSize = 1.f; style.TabRounding = 3.f; + style.DisplaySafeAreaPadding = GetDisplaySafeAreaPadding(); auto* colors = style.Colors; colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f); diff --git a/src/dusk/imgui/ImGuiEngine.hpp b/src/dusk/imgui/ImGuiEngine.hpp index 3b3e3e0303..44077c9625 100644 --- a/src/dusk/imgui/ImGuiEngine.hpp +++ b/src/dusk/imgui/ImGuiEngine.hpp @@ -25,4 +25,10 @@ struct Image { uint32_t height; }; Image GetImage(const std::string& path); + +#if (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || defined(__ANDROID__) +inline constexpr bool IsMobile = true; +#else +inline constexpr bool IsMobile = false; +#endif } // namespace dusk diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index c40e362db8..b0c20b4d46 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -1,6 +1,7 @@ #include "fmt/format.h" #include "imgui.h" +#include "ImGuiEngine.hpp" #include "ImGuiConsole.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiConfig.hpp" @@ -32,15 +33,17 @@ namespace dusk { void ImGuiMenuGame::draw() { if (ImGui::BeginMenu("Game")) { if (ImGui::BeginMenu("Graphics")) { - if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { - ToggleFullscreen(); - } + if (!IsMobile) { + if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { + ToggleFullscreen(); + } - if (ImGui::MenuItem("Default Window Size")) { - getSettings().video.enableFullscreen.setValue(false); - VISetWindowFullscreen(false); - VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); - VICenterWindow(); + if (ImGui::MenuItem("Default Window Size")) { + getSettings().video.enableFullscreen.setValue(false); + VISetWindowFullscreen(false); + VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); + VICenterWindow(); + } } bool vsync = getSettings().video.enableVsync; @@ -147,7 +150,7 @@ namespace dusk { JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; } - if (ImGui::MenuItem("Exit")) { + if (!IsMobile && ImGui::MenuItem("Exit")) { dusk::IsRunning = false; } diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp index c24dbd8247..ecbd41e158 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -4,13 +4,13 @@ #include "ImGuiEngine.hpp" #include "ImGuiPreLaunchWindow.hpp" +#include "../file_select.hpp" +#include "../iso_validate.hpp" #include "ImGuiConsole.hpp" #include "dusk/main.h" #include "dusk/settings.h" -#include "../iso_validate.hpp" #include -#include #include #include "aurora/lib/internal.hpp" @@ -46,30 +46,22 @@ static std::string ShowIsoInvalidError(const iso::ValidationError code) { } } -void fileDialogCallback(void* userdata, const char* const* filelist, [[maybe_unused]] int filter) { +void fileDialogCallback(void* userdata, const char* path, const char* error) { auto* self = static_cast(userdata); - self->m_errorString.clear(); - if (filelist != nullptr) { - if (filelist[0] == nullptr) { - // Cancelled - self->m_selectedIsoPath.clear(); - } else { - const auto path = filelist[0]; - const auto ret = iso::validate(path); - if (ret != iso::ValidationError::Success) { - self->m_selectedIsoPath.clear(); - self->m_errorString = std::move(ShowIsoInvalidError(ret)); - return; - } - self->m_selectedIsoPath = path; - getSettings().backend.isoPath.setValue(path); - config::Save(); - } - } else { - // Error occurred + if (error != nullptr) { self->m_selectedIsoPath.clear(); - self->m_errorString = fmt::format("File dialog error: {}", SDL_GetError()); + self->m_errorString = fmt::format("File dialog error: {}", error); + return; } + + if (path == nullptr) { + self->m_selectedIsoPath.clear(); + return; + } + + self->m_selectedIsoPath = path; + getSettings().backend.isoPath.setValue(self->m_selectedIsoPath); + config::Save(); } ImGuiPreLaunchWindow::ImGuiPreLaunchWindow() = default; @@ -144,9 +136,9 @@ void ImGuiPreLaunchWindow::drawMainMenu() { } if (ImGuiButtonCenter("Select disc image...")) { - SDL_ShowOpenFileDialog(&fileDialogCallback, this, aurora::window::get_sdl_window(), - skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), - nullptr, false); + ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), + skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, + false); } } else { if (ImGuiButtonCenter("Start game")) { @@ -187,9 +179,9 @@ void ImGuiPreLaunchWindow::drawOptions() { ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly); ImGui::SameLine(); if (ImGui::Button("Set")) { - SDL_ShowOpenFileDialog(&fileDialogCallback, this, aurora::window::get_sdl_window(), - skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), - nullptr, false); + ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), + skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, + false); } AuroraBackend configuredBackend = BACKEND_AUTO; diff --git a/src/dusk/ios/FileSelectDialog.h b/src/dusk/ios/FileSelectDialog.h new file mode 100644 index 0000000000..0495b397b1 --- /dev/null +++ b/src/dusk/ios/FileSelectDialog.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include + +struct SDL_Window; + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*IOSFileCallback)(void* userdata, const char* path, const char* error); + +void Dusk_iOS_ShowFileSelect(IOSFileCallback callback, void* userdata, SDL_Window* window, + const SDL_DialogFileFilter* filters, int nfilters, + const char* default_location, bool allow_many); + +#ifdef __cplusplus +} +#endif diff --git a/src/dusk/ios/FileSelectDialog.m b/src/dusk/ios/FileSelectDialog.m new file mode 100644 index 0000000000..e8efa91a69 --- /dev/null +++ b/src/dusk/ios/FileSelectDialog.m @@ -0,0 +1,151 @@ +#include "FileSelectDialog.h" + +#import +#import +#import +#import + +#include +#include +#include +#include + +static void *g_picker_delegate_key = &g_picker_delegate_key; + +static void RunOnMainThread(void (^block)(void)) +{ + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +static NSError *MakeError(NSString *message) +{ + return [NSError errorWithDomain:@"org.twilitrealm.dusk.file-select" + code:1 + userInfo:@{NSLocalizedDescriptionKey: message}]; +} + +static UIViewController *FindTopViewController(UIViewController *controller) +{ + UIViewController *current = controller; + while (current.presentedViewController != nil) { + current = current.presentedViewController; + } + return current; +} + +static UIViewController *PresenterFromWindow(SDL_Window *window) +{ + if (window == nil) { + return nil; + } + + const SDL_PropertiesID props = SDL_GetWindowProperties(window); + if (props == 0) { + return nil; + } + + UIWindow *uiwindow = (__bridge UIWindow *)SDL_GetPointerProperty( + props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, NULL); + if (uiwindow == nil || uiwindow.rootViewController == nil) { + return nil; + } + + return FindTopViewController(uiwindow.rootViewController); +} + +static NSURL *InitialDirectoryURL(const char *default_location) +{ + if (default_location == NULL || *default_location == '\0') { + return nil; + } + + NSString *path = [NSString stringWithUTF8String:default_location]; + NSURL *url = [NSURL fileURLWithPath:path]; + if ([path hasSuffix:@"/"]) { + return url; + } + + return [url URLByDeletingLastPathComponent]; +} + +@interface DocumentPickerDelegate : NSObject + +@property(nonatomic, assign) IOSFileCallback callback; +@property(nonatomic, assign) void *userdata; + +@end + +@implementation DocumentPickerDelegate + +- (void)finishWithPath:(const char *)path error:(const char *)error { + if (self.callback != NULL) { + self.callback(self.userdata, path, error); + } +} + +- (void)documentPicker:(UIDocumentPickerViewController *)controller +didPickDocumentsAtURLs:(NSArray *)urls +{ + NSURL *url = urls.firstObject; + if (url == nil) { + [self finishWithPath:NULL error:NULL]; + return; + } + + [self finishWithPath:url.path.UTF8String error:NULL]; + (void)controller; +} + +- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller +{ + [self finishWithPath:NULL error:NULL]; + (void)controller; +} + +@end + +void Dusk_iOS_ShowFileSelect(IOSFileCallback callback, void *userdata, + SDL_Window *window, + const SDL_DialogFileFilter *filters, int nfilters, + const char *default_location, + bool allow_many) +{ + RunOnMainThread(^{ + @autoreleasepool { + UIViewController *presenter = PresenterFromWindow(window); + if (presenter == nil) { + callback(userdata, NULL, "Failed to find an iOS view controller for the file picker."); + return; + } + + NSLog(@"[ShowFileSelect] presenting picker from %@", NSStringFromClass([presenter class])); + + UIDocumentPickerViewController *picker = + [[UIDocumentPickerViewController alloc] + initForOpeningContentTypes:@[ UTTypeItem ] + asCopy:YES]; + picker.allowsMultipleSelection = allow_many ? YES : NO; + picker.shouldShowFileExtensions = YES; + + NSURL *directory_url = InitialDirectoryURL(default_location); + if (directory_url != nil) { + picker.directoryURL = directory_url; + } + + DocumentPickerDelegate *delegate = [DocumentPickerDelegate new]; + delegate.callback = callback; + delegate.userdata = userdata; + picker.delegate = delegate; + objc_setAssociatedObject(picker, g_picker_delegate_key, delegate, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + [presenter presentViewController:picker animated:YES completion:nil]; + (void)filters; + (void)nfilters; + } + }); +} diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 715f9d3c84..2ee8c38711 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -133,6 +133,9 @@ bool launchUILoop() { const AuroraEvent* event = aurora_update(); while (event != nullptr && event->type != AURORA_NONE) { switch (event->type) { + case AURORA_SDL_EVENT: + dusk::g_imguiConsole.HandleSDLEvent(event->sdl); + break; case AURORA_WINDOW_RESIZED: preLaunchUIWindowSize = event->windowSize; break; @@ -213,6 +216,9 @@ void main01(void) { switch (event->type) { case AURORA_NONE: goto eventsDone; + case AURORA_SDL_EVENT: + dusk::g_imguiConsole.HandleSDLEvent(event->sdl); + break; case AURORA_WINDOW_RESIZED: mDoGph_gInf_c::setWindowSize(event->windowSize); break;