diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0607841922..37679e3f56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,6 +67,7 @@ jobs: run: ci/build-appimage.sh - name: Upload artifacts + if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}} @@ -76,24 +77,27 @@ jobs: build-apple: name: Build Apple (${{matrix.name}}) - if: 'false' # TODO enable when CI is free - runs-on: macos-latest + runs-on: [self-hosted, macOS] strategy: fail-fast: false matrix: include: - - name: AppleClang macOS universal + - name: AppleClang macOS arm64 platform: macos - preset: x-macos-ci - artifact_name: macos-appleclang-universal - - name: AppleClang iOS arm64 - platform: ios - preset: x-ios-ci - artifact_name: ios-appleclang-arm64 - - name: AppleClang tvOS arm64 - platform: tvos - preset: x-tvos-ci - artifact_name: tvos-appleclang-arm64 + preset: x-macos-ci-arm64 + artifact_name: macos-appleclang-arm64 + # - name: AppleClang macOS x86_64 + # platform: macos + # preset: x-macos-ci-x86_64 + # artifact_name: macos-appleclang-x86_64 + # - name: AppleClang iOS arm64 + # platform: ios + # preset: x-ios-ci + # artifact_name: ios-appleclang-arm64 + # - name: AppleClang tvOS arm64 + # platform: tvos + # preset: x-tvos-ci + # artifact_name: tvos-appleclang-arm64 steps: - uses: actions/checkout@v6 @@ -101,22 +105,15 @@ jobs: fetch-depth: 0 submodules: recursive - - name: Update Homebrew - if: matrix.platform == 'tvos' - run: | - brew update - brew upgrade --formula - - name: Install dependencies + if: 'false' run: brew install cmake ninja - - name: Install markupsafe - if: matrix.platform == 'tvos' - run: pip3 install --break-system-packages markupsafe - - name: Install Rust iOS target if: matrix.platform == 'ios' - run: rustup target add aarch64-apple-ios + run: | + rustup toolchain install stable + rustup target add aarch64-apple-ios - name: Install Rust tvOS target if: matrix.platform == 'tvos' @@ -124,6 +121,12 @@ jobs: rustup toolchain install nightly rustup target add --toolchain nightly aarch64-apple-tvos + - name: Install Rust x86_64 macOS target + if: endsWith(matrix.preset, 'x86_64') + run: | + rustup toolchain install stable + rustup target add x86_64-apple-darwin + - name: Setup sccache uses: mozilla-actions/sccache-action@v0.0.9 @@ -134,6 +137,7 @@ jobs: run: cmake --build --preset ${{matrix.preset}} - name: Upload artifacts + if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}} @@ -199,6 +203,7 @@ jobs: run: cmake --build --preset x-windows-ci-${{matrix.preset}} - name: Upload artifacts + if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}} diff --git a/CMakeLists.txt b/CMakeLists.txt index 1dbaefd7d3..c99133738b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,9 @@ endif() message(STATUS "Dusk version set to ${DUSK_WC_DESCRIBE}") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") project(dusk LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING}) +if (APPLE) + enable_language(OBJC) +endif () if (APPLE AND NOT TVOS AND CMAKE_SYSTEM_NAME STREQUAL tvOS) # ios.toolchain.cmake hack for SDL set(TVOS ON) @@ -125,19 +128,37 @@ if (DUSK_MOVIE_SUPPORT) else () set(_jpeg_lib ${_jpeg_install_dir}/lib/libturbojpeg.a) endif () + set(_jpeg_cmake_args + -DCMAKE_INSTALL_PREFIX=${_jpeg_install_dir} + -DENABLE_SHARED=OFF + -DWITH_TURBOJPEG=ON + -DWITH_JAVA=OFF + ) + if (CMAKE_TOOLCHAIN_FILE) + get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}") + list(APPEND _jpeg_cmake_args -DCMAKE_TOOLCHAIN_FILE=${_jpeg_toolchain_file}) + endif () + set(_jpeg_passthrough_vars + CMAKE_BUILD_TYPE + CMAKE_C_COMPILER + CMAKE_C_COMPILER_LAUNCHER + CMAKE_MAKE_PROGRAM + CMAKE_MSVC_RUNTIME_LIBRARY + CMAKE_OSX_ARCHITECTURES + DEPLOYMENT_TARGET + ENABLE_ARC + ENABLE_BITCODE + PLATFORM + ) + foreach(_var IN LISTS _jpeg_passthrough_vars) + if (DEFINED ${_var}) + list(APPEND _jpeg_cmake_args -D${_var}=${${_var}}) + endif () + endforeach () ExternalProject_Add(libjpeg-turbo-ext URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.1.0.tar.gz URL_HASH SHA256=35fec2e1ddfb05ecf6d93e50bc57c1e54bc81c16d611ddf6eff73fff266d8285 - CMAKE_ARGS - -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - -DCMAKE_INSTALL_PREFIX=${_jpeg_install_dir} - -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} - -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} - -DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER} - -DCMAKE_MSVC_RUNTIME_LIBRARY=${CMAKE_MSVC_RUNTIME_LIBRARY} - -DENABLE_SHARED=OFF - -DWITH_TURBOJPEG=ON - -DWITH_JAVA=OFF + CMAKE_ARGS ${_jpeg_cmake_args} BUILD_BYPRODUCTS ${_jpeg_lib} ) file(MAKE_DIRECTORY ${_jpeg_install_dir}/include) @@ -249,7 +270,7 @@ include(files.cmake) # TODO: version handling for res includes set(DUSK_BUNDLE_NAME Dusk) -set(DUSK_BUNDLE_IDENTIFIER dev.decomp.dusk) +set(DUSK_BUNDLE_IDENTIFIER dev.twilitrealm.dusk) set(DUSK_COMPANY_NAME "Twilit Realm") set(DUSK_FILE_DESCRIPTION "Dusk") set(DUSK_PRODUCT_NAME "Dusk") @@ -277,7 +298,7 @@ set(GAME_INCLUDE_DIRS ${CMAKE_BINARY_DIR}) set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd - aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient) + aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt) list(APPEND GAME_LIBS libzstd_static) @@ -362,12 +383,14 @@ if (ANDROID) target_link_options(dusk PRIVATE "-Wl,-u,SDL_main") endif () -add_custom_command(TARGET dusk POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CMAKE_SOURCE_DIR}/res" - "$/res" - COMMENT "Copying resources" -) +if (NOT APPLE) + add_custom_command(TARGET dusk POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/res" + "$/res" + COMMENT "Copying resources" + ) +endif () if (WIN32) set(DUSK_WINDOWS_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/windows) @@ -401,44 +424,49 @@ endif () if (APPLE) if (IOS) set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios) - set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in) - file(GLOB_RECURSE DUSK_RESOURCE_FILES "${DUSK_RESOURCE_DIR}/Base.lproj/*") - endif () - if (IOS OR TVOS) - target_sources(dusk PRIVATE ${DUSK_RESOURCE_FILES}) - foreach (FILE ${DUSK_RESOURCE_FILES}) - file(RELATIVE_PATH NEW_FILE "${DUSK_RESOURCE_DIR}" ${FILE}) - get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY) - set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}") - endforeach () - set_target_properties( - dusk PROPERTIES - MACOSX_BUNDLE TRUE - MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME} - MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER} - MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING} - MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING} - OUTPUT_NAME dusk - XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES" - XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES" - ) - if (CMAKE_GENERATOR STREQUAL "Xcode") - set_target_properties(dusk PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST}) - elseif (DEFINED DUSK_INFO_PLIST) - set(MACOSX_BUNDLE_EXECUTABLE_NAME dusk) - set(MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}) - set(MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}) - set(MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING}) - set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING}) - set(DUSK_GENERATED_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/dusk.Info.plist) - configure_file(${DUSK_INFO_PLIST} ${DUSK_GENERATED_INFO_PLIST}) - add_custom_command( - TARGET dusk POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${DUSK_GENERATED_INFO_PLIST} $/Info.plist - VERBATIM - ) - endif () + elseif (TVOS) + set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/tvos) + else () + set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos) endif () + set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in) + file(GLOB_RECURSE DUSK_RESOURCE_FILES + "${DUSK_RESOURCE_DIR}/Assets.car" + "${DUSK_RESOURCE_DIR}/Base.lproj/*" + "${DUSK_RESOURCE_DIR}/Dusk.icns") + file(GLOB_RECURSE DUSK_APP_RESOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/*") + target_sources(dusk PRIVATE ${DUSK_RESOURCE_FILES}) + target_sources(dusk PRIVATE ${DUSK_APP_RESOURCE_FILES}) + foreach (FILE ${DUSK_RESOURCE_FILES}) + file(RELATIVE_PATH NEW_FILE "${DUSK_RESOURCE_DIR}" ${FILE}) + get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY) + set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}") + endforeach () + foreach (FILE ${DUSK_APP_RESOURCE_FILES}) + file(RELATIVE_PATH NEW_FILE "${CMAKE_CURRENT_SOURCE_DIR}" ${FILE}) + get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY) + set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}") + endforeach () + set_target_properties( + dusk PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME} + MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER} + MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING} + MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST} + OUTPUT_NAME Dusk + XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES" + XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES" + ) +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) @@ -487,7 +515,9 @@ if (TARGET crashpad_handler) endif () install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_PREFIX}) aurora_install_runtime_dlls(dusk ${CMAKE_INSTALL_PREFIX}) -install(DIRECTORY ${CMAKE_SOURCE_DIR}/res DESTINATION ${CMAKE_INSTALL_PREFIX}) +if (NOT APPLE) + install(DIRECTORY ${CMAKE_SOURCE_DIR}/res DESTINATION ${CMAKE_INSTALL_PREFIX}) +endif () if (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo) set(DEBUG_FILES_LIST "") foreach (target IN LISTS BINARY_TARGETS EXTRA_TARGETS) @@ -509,18 +539,22 @@ if (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo) endif () list(APPEND DEBUG_FILES_LIST "${output_name}") endforeach () - if (WIN32) - list(TRANSFORM DEBUG_FILES_LIST APPEND ".pdb") - list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES) - install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND 7z a -t7z \"${CMAKE_INSTALL_PREFIX}/debug.7z\" ${DEBUG_FILES})") - elseif (APPLE) - list(TRANSFORM DEBUG_FILES_LIST APPEND ".dSYM") - list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES) - install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND tar acfv \"${CMAKE_INSTALL_PREFIX}/debug.tar.xz\" ${DEBUG_FILES})") - elseif (UNIX) - list(TRANSFORM DEBUG_FILES_LIST APPEND ".dbg") - list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES) - install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND tar -I \"xz -9 -T0\" -cvf \"${CMAKE_INSTALL_PREFIX}/debug.tar.xz\" ${DEBUG_FILES})") + # This is a terrible hack to only run this on CI + # until I turn this into a script or something + if(DEFINED ENV{GITHUB_ENV}) + if (WIN32) + list(TRANSFORM DEBUG_FILES_LIST APPEND ".pdb") + list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES) + install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND 7z a -t7z \"${CMAKE_INSTALL_PREFIX}/debug.7z\" ${DEBUG_FILES})") + elseif (APPLE) + list(TRANSFORM DEBUG_FILES_LIST APPEND ".dSYM") + list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES) + install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND tar acfv \"${CMAKE_INSTALL_PREFIX}/debug.tar.xz\" ${DEBUG_FILES})") + elseif (UNIX) + list(TRANSFORM DEBUG_FILES_LIST APPEND ".dbg") + list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES) + install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND tar -I \"xz -9 -T0\" -cvf \"${CMAKE_INSTALL_PREFIX}/debug.tar.xz\" ${DEBUG_FILES})") + endif () endif () endif () foreach (target IN LISTS BINARY_TARGETS) diff --git a/CMakePresets.json b/CMakePresets.json index 44e75973b9..90307aa061 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -380,7 +380,40 @@ "inherits": [ "macos-default-relwithdebinfo", "ci" - ] + ], + "cacheVariables": { + "AURORA_DAWN_PROVIDER": "vendor", + "AURORA_NOD_PROVIDER": "vendor", + "CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": { + "type": "BOOL", + "value": true + }, + "CMAKE_OSX_DEPLOYMENT_TARGET": "11.0", + "CMAKE_IGNORE_PREFIX_PATH": "/opt/homebrew", + "DUSK_MOVIE_SUPPORT": { + "type": "BOOL", + "value": false + } + } + }, + { + "name": "x-macos-ci-arm64", + "inherits": [ + "x-macos-ci" + ], + "cacheVariables": { + "CMAKE_OSX_ARCHITECTURES": "arm64" + } + }, + { + "name": "x-macos-ci-x86_64", + "inherits": [ + "x-macos-ci" + ], + "cacheVariables": { + "CMAKE_OSX_ARCHITECTURES": "x86_64", + "Rust_CARGO_TARGET": "x86_64-apple-darwin" + } }, { "name": "x-ios-ci", @@ -389,7 +422,11 @@ ], "cacheVariables": { "CMAKE_C_COMPILER_LAUNCHER": "sccache", - "CMAKE_CXX_COMPILER_LAUNCHER": "sccache" + "CMAKE_CXX_COMPILER_LAUNCHER": "sccache", + "DUSK_MOVIE_SUPPORT": { + "type": "BOOL", + "value": false + } } }, { @@ -399,7 +436,11 @@ ], "cacheVariables": { "CMAKE_C_COMPILER_LAUNCHER": "sccache", - "CMAKE_CXX_COMPILER_LAUNCHER": "sccache" + "CMAKE_CXX_COMPILER_LAUNCHER": "sccache", + "DUSK_MOVIE_SUPPORT": { + "type": "BOOL", + "value": false + } } }, { @@ -556,10 +597,19 @@ ] }, { - "name": "x-macos-ci", - "configurePreset": "x-macos-ci", - "description": "(Internal) macOS CI", - "displayName": "(Internal) macOS CI", + "name": "x-macos-ci-arm64", + "configurePreset": "x-macos-ci-arm64", + "description": "(Internal) macOS CI arm64", + "displayName": "(Internal) macOS CI arm64", + "targets": [ + "install" + ] + }, + { + "name": "x-macos-ci-x86_64", + "configurePreset": "x-macos-ci-x86_64", + "description": "(Internal) macOS CI x86_64", + "displayName": "(Internal) macOS CI x86_64", "targets": [ "install" ] @@ -567,8 +617,8 @@ { "name": "x-ios-ci", "configurePreset": "x-ios-ci", - "description": "(Internal) iOS CI", - "displayName": "(Internal) iOS CI", + "description": "(Internal) iOS CI arm64", + "displayName": "(Internal) iOS CI arm64", "targets": [ "install" ] @@ -576,8 +626,8 @@ { "name": "x-tvos-ci", "configurePreset": "x-tvos-ci", - "description": "(Internal) tvOS CI", - "displayName": "(Internal) tvOS CI", + "description": "(Internal) tvOS CI arm64", + "displayName": "(Internal) tvOS CI arm64", "targets": [ "install" ] diff --git a/files.cmake b/files.cmake index 61a6f39cfd..ec7d097e21 100644 --- a/files.cmake +++ b/files.cmake @@ -1343,6 +1343,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/include/m_Do/m_Do_graphic.h b/include/m_Do/m_Do_graphic.h index c6b22654fa..f921912656 100644 --- a/include/m_Do/m_Do_graphic.h +++ b/include/m_Do/m_Do_graphic.h @@ -125,8 +125,15 @@ public: #if TARGET_PC static f32 hudAspectScaleDown; static f32 hudAspectScaleUp; - static f32 ScaleHUDXLeft(f32 baseX) { return getMinXF() + baseX; } - static f32 ScaleHUDXRight(f32 baseX) { return -getMinXF() + baseX; } + static void updateSafeAreaBounds(); + static f32 getSafeMinXF() { return m_safeMinXF; } + static f32 getSafeMinYF() { return m_safeMinYF; } + static f32 getSafeWidthF() { return m_safeWidthF; } + static f32 getSafeHeightF() { return m_safeHeightF; } + static f32 getSafeMaxXF() { return m_safeMaxXF; } + static f32 getSafeMaxYF() { return m_safeMaxYF; } + static f32 ScaleHUDXLeft(f32 baseX) { return getSafeMinXF() + baseX; } + static f32 ScaleHUDXRight(f32 baseX) { return getSafeMaxXF() - FB_WIDTH_BASE + baseX; } #endif static void setBlureMtx(const Mtx m) { @@ -369,6 +376,15 @@ public: static int m_height; static f32 m_heightF; static f32 m_widthF; + + #if TARGET_PC + static f32 m_safeMinXF; + static f32 m_safeMinYF; + static f32 m_safeMaxXF; + static f32 m_safeMaxYF; + static f32 m_safeWidthF; + static f32 m_safeHeightF; + #endif #endif }; diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml index d09b0d2785..e8c2bb29a8 100644 --- a/platforms/android/app/src/main/AndroidManifest.xml +++ b/platforms/android/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ android:allowBackup="true" android:hardwareAccelerated="true" android:appCategory="game" - android:icon="@android:drawable/sym_def_app_icon" + android:icon="@mipmap/icon" android:label="@string/app_name" android:theme="@android:style/Theme.NoTitleBar" android:enableOnBackInvokedCallback="false"> diff --git a/platforms/android/app/src/main/res/mipmap/icon.png b/platforms/android/app/src/main/res/mipmap/icon.png new file mode 100644 index 0000000000..05e11c9b41 Binary files /dev/null and b/platforms/android/app/src/main/res/mipmap/icon.png differ diff --git a/platforms/ios/Assets.car b/platforms/ios/Assets.car new file mode 100644 index 0000000000..5087cfe981 Binary files /dev/null and b/platforms/ios/Assets.car differ diff --git a/platforms/macos/Assets.car b/platforms/macos/Assets.car new file mode 100644 index 0000000000..b835eb03e0 Binary files /dev/null and b/platforms/macos/Assets.car differ diff --git a/platforms/macos/Dusk.icns b/platforms/macos/Dusk.icns new file mode 100644 index 0000000000..c684d8194f Binary files /dev/null and b/platforms/macos/Dusk.icns differ diff --git a/platforms/macos/Info.plist.in b/platforms/macos/Info.plist.in index c91435dc2e..b7bc7b706c 100644 --- a/platforms/macos/Info.plist.in +++ b/platforms/macos/Info.plist.in @@ -13,7 +13,9 @@ CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleIconFile - mainicon.icns + Dusk + CFBundleIconName + Dusk CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleName diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index ac28e46838..c3fcc806d5 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -147,7 +147,7 @@ void dBrightCheck_c::modeMove() { void dBrightCheck_c::brightCheckWide() { // Main Canvas mBrightCheck.Scr->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); - mBrightCheck.Scr->translate(mDoGph_gInf_c::getMinXF(), 0.0f); + mBrightCheck.Scr->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); // Right Square mBrightCheck.Scr->search(MULTI_CHAR('fuchi_1'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index be40b13d1c..550c5b3e52 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -3776,7 +3776,7 @@ bool dFile_select_c::yesnoWakuAlpahAnm(u8 param_1) { #if TARGET_PC void dFile_select_c::fileSelectWide() { mYnSel.ScrYn->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); - mYnSel.ScrYn->translate(mDoGph_gInf_c::getMinXF(), 0.0f); + mYnSel.ScrYn->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); mYnSel.ScrYn->search(MULTI_CHAR('w_no_t'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); mYnSel.ScrYn->search(MULTI_CHAR('f_no_t'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); @@ -3784,7 +3784,7 @@ void dFile_select_c::fileSelectWide() { mYnSel.ScrYn->search(MULTI_CHAR('f_yes_t'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); m3mSel.Scr3m->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); - m3mSel.Scr3m->translate(mDoGph_gInf_c::getMinXF(), 0.0f); + m3mSel.Scr3m->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); m3mSel.Scr3m->search(MULTI_CHAR('w_sta'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); m3mSel.Scr3m->search(MULTI_CHAR('f_sta'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); @@ -3794,7 +3794,7 @@ void dFile_select_c::fileSelectWide() { m3mSel.Scr3m->search(MULTI_CHAR('f_cop_t'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); fileSel.Scr->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); - fileSel.Scr->translate(mDoGph_gInf_c::getMinXF(), 0.0f); + fileSel.Scr->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); fileSel.Scr->search(MULTI_CHAR('t_for'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); fileSel.Scr->search(MULTI_CHAR('t_for1'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); @@ -5584,7 +5584,13 @@ void dFile_select3D_c::createMirrorModel() { void dFile_select3D_c::toItem3Dpos(f32 param_0, f32 param_1, f32 param_2, cXyz* param_3) { Mtx adStack_98; Mtx auStack_c8; +#if TARGET_PC + param_0 = + (2.0f * ((param_0 - mDoGph_gInf_c::getSafeMinXF()) / mDoGph_gInf_c::getSafeWidthF()) - + 1.0f); +#else param_0 = (2.0f * ((param_0 - mDoGph_gInf_c::getMinXF()) / mDoGph_gInf_c::getWidthF()) - 1.0f); +#endif param_1 = (2.0f * ((param_1 - -100.0f) / FB_HEIGHT_BASE) - 1.0f); calcViewMtx(adStack_98); cMtx_inverse(adStack_98, auStack_c8); @@ -5601,4 +5607,3 @@ void dFile_select3D_c::calcViewMtx(Mtx param_0) { cXyz pos2(0.0f, 1.0f, 0.0f); cMtx_lookAt(param_0, &pos1, &cXyz::Zero, &pos2, 0); } - diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index c7e5bb7414..7ae416c4be 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -99,7 +99,7 @@ dMenu_Collect2D_c::~dMenu_Collect2D_c() { void dMenu_Collect2D_c::menuCollectWide() { // Main Canvas mpScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); - mpScreen->translate(mDoGph_gInf_c::getMinXF(), 0.0f); + mpScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); // Pieces of Heart mpScreen->search(MULTI_CHAR('heart_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); @@ -182,7 +182,7 @@ void dMenu_Collect2D_c::_create() { } #if TARGET_PC - mpScreenIcon->translate(-mDoGph_gInf_c::getMinXF(), 0.0f); + mpScreenIcon->translate(-mDoGph_gInf_c::getSafeMinXF(), 0.0f); #endif dPaneClass_showNullPane(mpScreenIcon); @@ -2706,8 +2706,14 @@ void dMenu_Collect3D_c::setupItem3D(Mtx param_0) { void dMenu_Collect3D_c::toItem3Dpos(f32 param_0, f32 param_1, f32 param_2, cXyz* param_3) { Mtx adStack_98; Mtx auStack_c8; +#if TARGET_PC + param_0 = + (2.0f * ((param_0 - mDoGph_gInf_c::getSafeMinXF()) / mDoGph_gInf_c::getSafeWidthF()) - + 1.0f); +#else param_0 = (2.0f * ((param_0 - mDoGph_gInf_c::getMinXF()) / mDoGph_gInf_c::getWidthF()) - 1.0f); +#endif param_1 = (2.0f * ((param_1 - -100.0f) / FB_HEIGHT_BASE) - 1.0f); calcViewMtx(adStack_98); MTXInverse(adStack_98, auStack_c8); diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index 618677866b..0935eb4e52 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -2782,7 +2782,7 @@ void dMenu_save_c::_draw() { #if TARGET_PC void dMenu_save_c::menuSaveWide() { mSaveSel.Scr->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); - mSaveSel.Scr->translate(mDoGph_gInf_c::getMinXF(), 0.0f); + mSaveSel.Scr->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); mSaveSel.Scr->search(MULTI_CHAR('t_for'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); mSaveSel.Scr->search(MULTI_CHAR('t_for1'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); 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_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index d774a7de7d..a56e448645 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -47,6 +47,8 @@ #endif #if TARGET_PC +#include +#include "aurora/lib/window.hpp" #include "d/actor/d_a_horse.h" #include "dusk/dusk.h" #include "dusk/endian.h" @@ -636,8 +638,9 @@ void mDoGph_gInf_c::setTvSize() { m_invScale = 1.0f / m_scale; #if TARGET_PC - hudAspectScaleDown = 1.3571428f / mDoGph_gInf_c::getAspect(); - hudAspectScaleUp = 1.0f / hudAspectScaleDown; + updateSafeAreaBounds(); + hudAspectScaleUp = getSafeWidthF() / FB_WIDTH_BASE; + hudAspectScaleDown = FB_WIDTH_BASE / getSafeWidthF(); #endif } @@ -765,6 +768,88 @@ void mDoGph_gInf_c::setWideZoomLightProjection(Mtx& m) { #if TARGET_PC f32 mDoGph_gInf_c::hudAspectScaleDown = 1.0f; f32 mDoGph_gInf_c::hudAspectScaleUp = 1.0f; +f32 mDoGph_gInf_c::m_safeMinXF = 0.0f; +f32 mDoGph_gInf_c::m_safeMinYF = 0.0f; +f32 mDoGph_gInf_c::m_safeMaxXF = FB_WIDTH_BASE; +f32 mDoGph_gInf_c::m_safeMaxYF = FB_HEIGHT_BASE; +f32 mDoGph_gInf_c::m_safeWidthF = FB_WIDTH_BASE; +f32 mDoGph_gInf_c::m_safeHeightF = FB_HEIGHT_BASE; + +void mDoGph_gInf_c::updateSafeAreaBounds() { + m_safeMinXF = m_minXF; + m_safeMinYF = m_minYF; + m_safeMaxXF = m_maxXF; + m_safeMaxYF = m_maxYF; + m_safeWidthF = m_widthF; + m_safeHeightF = m_heightF; + + SDL_Window* window = aurora::window::get_sdl_window(); + if (window == NULL) { + return; + } + + const AuroraWindowSize windowSize = aurora::window::get_window_size(); + const f32 windowWidth = static_cast(windowSize.width); + const f32 windowHeight = static_cast(windowSize.height); + if (windowWidth <= 0.0f || windowHeight <= 0.0f) { + return; + } + + SDL_Rect safeRect{}; + if (!SDL_GetWindowSafeArea(window, &safeRect)) { + return; + } + + if (windowSize.native_fb_width == 0 || windowSize.native_fb_height == 0 || + windowSize.fb_width == 0 || windowSize.fb_height == 0) + { + return; + } + + const f32 nativeScaleX = static_cast(windowSize.native_fb_width) / windowWidth; + const f32 nativeScaleY = static_cast(windowSize.native_fb_height) / windowHeight; + + const f32 safeLeft = static_cast(safeRect.x) * nativeScaleX; + const f32 safeTop = static_cast(safeRect.y) * nativeScaleY; + const f32 safeRight = static_cast(safeRect.x + safeRect.w) * nativeScaleX; + const f32 safeBottom = static_cast(safeRect.y + safeRect.h) * nativeScaleY; + + const f32 viewportLeft = + (static_cast(windowSize.native_fb_width) - static_cast(windowSize.fb_width)) * + 0.5f; + const f32 viewportTop = + (static_cast(windowSize.native_fb_height) - static_cast(windowSize.fb_height)) * + 0.5f; + const f32 viewportRight = viewportLeft + static_cast(windowSize.fb_width); + const f32 viewportBottom = viewportTop + static_cast(windowSize.fb_height); + + const f32 leftInset = std::max(0.0f, safeLeft - viewportLeft) * + (m_widthF / static_cast(windowSize.fb_width)); + const f32 topInset = std::max(0.0f, safeTop - viewportTop) * + (m_heightF / static_cast(windowSize.fb_height)); + const f32 rightInset = std::max(0.0f, viewportRight - safeRight) * + (m_widthF / static_cast(windowSize.fb_width)); + const f32 bottomInset = std::max(0.0f, viewportBottom - safeBottom) * + (m_heightF / static_cast(windowSize.fb_height)); + + const f32 safeMinXF = m_minXF + leftInset; + const f32 safeMinYF = m_minYF + topInset; + const f32 safeMaxXF = m_maxXF - rightInset; + const f32 safeMaxYF = m_maxYF - bottomInset; + const f32 safeWidthF = safeMaxXF - safeMinXF; + const f32 safeHeightF = safeMaxYF - safeMinYF; + + if (safeWidthF <= 0.0f || safeHeightF <= 0.0f) { + return; + } + + m_safeMinXF = safeMinXF; + m_safeMinYF = safeMinYF; + m_safeMaxXF = safeMaxXF; + m_safeMaxYF = safeMaxYF; + m_safeWidthF = safeWidthF; + m_safeHeightF = safeHeightF; +} void mDoGph_gInf_c::setWindowSize(AuroraWindowSize const& size) { JUTVideo::getManager()->setWindowSize(size); diff --git a/src/m_Do/m_Do_lib.cpp b/src/m_Do/m_Do_lib.cpp index 93df5c23cc..b541490353 100644 --- a/src/m_Do/m_Do_lib.cpp +++ b/src/m_Do/m_Do_lib.cpp @@ -154,7 +154,7 @@ void mDoLib_project(Vec* src, Vec* dst, JGeometry::TBox2 viewport) { xSize = FB_WIDTH; } else { #if TARGET_PC - xOffset = mDoGph_gInf_c::getMinXF(); + xOffset = mDoGph_gInf_c::getSafeMinXF(); xSize = viewport.f.x * mDoGph_gInf_c::hudAspectScaleUp; #else xOffset = viewport.i.x; diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 1fed646a71..784aea3f26 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;