From 3beb147c55a800494c83d24c7f1ccdf91a0d9551 Mon Sep 17 00:00:00 2001 From: freakdaniel Date: Sat, 2 May 2026 04:00:24 +0300 Subject: [PATCH] Refactor: InfiniFrame.Native - Introduced InfiniFrameNativeStatusCode enum to represent native call status - Updated InfiniFrameNative methods to return status codes instead of void - Added EnsureSucceeded method to handle error checking and throw exceptions with detailed messages - Modified InvokeUtilities to use status codes in callbacks - Updated MonitorsUtility and InfiniFrameWindow to handle status codes and ensure proper error handling - Added tests for native export guards to validate error handling and status codes - Divide Exports to logical code-zones - Update clang and testing utilities - Add native_quality.sh for QA CI - Update CMakeLists for new native structure - Create & Update Interop layer --- .../setup-dependencies-native/action.yml | 2 + .github/scripts/native_quality.sh | 119 +++ .github/workflows/shared-testing-linux.yml | 18 +- .github/workflows/shared-testing-macos.yml | 5 +- .github/workflows/shared-testing-windows.yml | 6 +- src/InfiniFrame.Native/CMakeLists.txt | 55 +- src/InfiniFrame.Native/Exports.cpp | 770 -------------- .../InfiniFrame.Native.proj | 9 +- src/InfiniFrame.Native/Interop/ExportApi.h | 20 + .../Interop/Exports.Browser.cpp | 87 ++ .../Interop/Exports.Dialog.cpp | 109 ++ .../Interop/Exports.Events.cpp | 100 ++ .../Interop/Exports.Lifecycle.cpp | 38 + .../Interop/Exports.Memory.cpp | 51 + .../Interop/Exports.Platform.cpp | 62 ++ .../Interop/Exports.WindowCommands.cpp | 55 + src/InfiniFrame.Native/Interop/Exports.cpp | 503 +++++++++ .../Interop/InitParamsReader.h | 52 + src/InfiniFrame.Native/Interop/NativeBuffer.h | 43 + src/InfiniFrame.Native/Interop/NativeResult.h | 217 ++++ src/InfiniFrame.Native/Interop/NativeString.h | 99 ++ .../Platform/Linux/Dialog.cpp | 18 +- .../Platform/Linux/Monitors.Gtk.cpp | 33 + .../Linux/Notifications.LibNotify.cpp | 26 + .../Platform/Linux/UiDispatcher.Gtk.cpp | 47 + .../Platform/Linux/WebKitBridge.Gtk.cpp | 69 ++ .../Linux/WebKitCustomSchemes.Gtk.cpp | 80 ++ .../Platform/Linux/WebKitMessaging.Gtk.cpp | 122 +++ .../Platform/Linux/WebKitMessaging.Gtk.h | 13 + .../Platform/Linux/WebKitSettings.Gtk.cpp | 115 +++ .../Platform/Linux/Window.cpp | 549 ++-------- .../Platform/Linux/WindowImpl.Gtk.h | 52 + .../Platform/Linux/WindowState.Gtk.cpp | 36 + src/InfiniFrame.Native/Platform/Mac/Dialog.mm | 11 +- .../Platform/Mac/Monitors.Cocoa.mm | 34 + .../Notifications.UserNotifications.Cocoa.mm | 24 + .../Platform/Mac/UiDispatcher.Cocoa.mm | 20 + .../Platform/Mac/UrlSchemeHandler.mm | 20 +- .../Platform/Mac/WKCustomSchemes.Cocoa.mm | 32 + .../Platform/Mac/WKJsInterop.Cocoa.mm | 29 + .../Platform/Mac/WKWebViewBridge.Cocoa.mm | 78 ++ .../Platform/Mac/WKWebViewSettings.Cocoa.mm | 102 ++ src/InfiniFrame.Native/Platform/Mac/Window.mm | 434 +------- .../Platform/Mac/WindowImpl.Cocoa.h | 44 + .../Platform/Mac/WindowState.Cocoa.mm | 84 ++ .../Platform/Windows/Dialog.cpp | 10 +- .../Platform/Windows/Monitors.Win32.cpp | 44 + .../Windows/Notifications.WinToast.cpp | 42 + .../Platform/Windows/UiDispatcher.Win32.cpp | 34 + .../Platform/Windows/WebView2Bridge.Win32.cpp | 64 ++ .../Windows/WebView2CustomSchemes.Win32.cpp | 67 ++ .../Windows/WebView2CustomSchemes.Win32.h | 22 + .../Platform/Windows/WebView2Host.Win32.cpp | 288 ++++++ .../Windows/WebView2Messaging.Win32.cpp | 39 + .../WebView2ResourceRequests.Win32.cpp | 149 +++ .../Windows/WebView2Settings.Win32.cpp | 38 + .../Platform/Windows/Window.cpp | 963 +----------------- .../Platform/Windows/WindowImpl.Win32.h | 88 ++ .../Platform/Windows/WindowProc.Win32.cpp | 230 +++++ .../Platform/Windows/WindowProc.Win32.h | 38 + .../Shared/CustomSchemeResponse.h | 94 ++ src/InfiniFrame.Native/Utils/Common.h | 19 +- .../cmake/Platform.MacOS.cmake | 31 +- .../FluentApi/InfiniWindowExtensions.cs | 129 +-- .../Native/InfiniFrameNative.cs | 232 +++-- .../Native/InfiniFrameNativeStatusCode.cs | 7 + src/InfiniFrame.Shared/Native/NativeDll.cs | 1 + .../Utilities/InvokeUtilities.cs | 7 +- .../Utilities/MonitorsUtility.cs | 4 +- src/InfiniFrame/InfiniFrameWindow.cs | 47 +- .../InfiniFrameNativeExportGuardTests.cs | 95 ++ 71 files changed, 4449 insertions(+), 2825 deletions(-) create mode 100644 .github/scripts/native_quality.sh delete mode 100644 src/InfiniFrame.Native/Exports.cpp create mode 100644 src/InfiniFrame.Native/Interop/ExportApi.h create mode 100644 src/InfiniFrame.Native/Interop/Exports.Browser.cpp create mode 100644 src/InfiniFrame.Native/Interop/Exports.Dialog.cpp create mode 100644 src/InfiniFrame.Native/Interop/Exports.Events.cpp create mode 100644 src/InfiniFrame.Native/Interop/Exports.Lifecycle.cpp create mode 100644 src/InfiniFrame.Native/Interop/Exports.Memory.cpp create mode 100644 src/InfiniFrame.Native/Interop/Exports.Platform.cpp create mode 100644 src/InfiniFrame.Native/Interop/Exports.WindowCommands.cpp create mode 100644 src/InfiniFrame.Native/Interop/Exports.cpp create mode 100644 src/InfiniFrame.Native/Interop/InitParamsReader.h create mode 100644 src/InfiniFrame.Native/Interop/NativeBuffer.h create mode 100644 src/InfiniFrame.Native/Interop/NativeResult.h create mode 100644 src/InfiniFrame.Native/Interop/NativeString.h create mode 100644 src/InfiniFrame.Native/Platform/Linux/Monitors.Gtk.cpp create mode 100644 src/InfiniFrame.Native/Platform/Linux/Notifications.LibNotify.cpp create mode 100644 src/InfiniFrame.Native/Platform/Linux/UiDispatcher.Gtk.cpp create mode 100644 src/InfiniFrame.Native/Platform/Linux/WebKitBridge.Gtk.cpp create mode 100644 src/InfiniFrame.Native/Platform/Linux/WebKitCustomSchemes.Gtk.cpp create mode 100644 src/InfiniFrame.Native/Platform/Linux/WebKitMessaging.Gtk.cpp create mode 100644 src/InfiniFrame.Native/Platform/Linux/WebKitMessaging.Gtk.h create mode 100644 src/InfiniFrame.Native/Platform/Linux/WebKitSettings.Gtk.cpp create mode 100644 src/InfiniFrame.Native/Platform/Linux/WindowImpl.Gtk.h create mode 100644 src/InfiniFrame.Native/Platform/Linux/WindowState.Gtk.cpp create mode 100644 src/InfiniFrame.Native/Platform/Mac/Monitors.Cocoa.mm create mode 100644 src/InfiniFrame.Native/Platform/Mac/Notifications.UserNotifications.Cocoa.mm create mode 100644 src/InfiniFrame.Native/Platform/Mac/UiDispatcher.Cocoa.mm create mode 100644 src/InfiniFrame.Native/Platform/Mac/WKCustomSchemes.Cocoa.mm create mode 100644 src/InfiniFrame.Native/Platform/Mac/WKJsInterop.Cocoa.mm create mode 100644 src/InfiniFrame.Native/Platform/Mac/WKWebViewBridge.Cocoa.mm create mode 100644 src/InfiniFrame.Native/Platform/Mac/WKWebViewSettings.Cocoa.mm create mode 100644 src/InfiniFrame.Native/Platform/Mac/WindowImpl.Cocoa.h create mode 100644 src/InfiniFrame.Native/Platform/Mac/WindowState.Cocoa.mm create mode 100644 src/InfiniFrame.Native/Platform/Windows/Monitors.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/Notifications.WinToast.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/UiDispatcher.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/WebView2Bridge.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/WebView2CustomSchemes.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/WebView2CustomSchemes.Win32.h create mode 100644 src/InfiniFrame.Native/Platform/Windows/WebView2Host.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/WebView2Messaging.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/WebView2ResourceRequests.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/WebView2Settings.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/WindowImpl.Win32.h create mode 100644 src/InfiniFrame.Native/Platform/Windows/WindowProc.Win32.cpp create mode 100644 src/InfiniFrame.Native/Platform/Windows/WindowProc.Win32.h create mode 100644 src/InfiniFrame.Native/Shared/CustomSchemeResponse.h create mode 100644 src/InfiniFrame.Shared/Native/InfiniFrameNativeStatusCode.cs create mode 100644 tests/InfiniFrameTests/InfiniFrameNativeExportGuardTests.cs diff --git a/.github/actions/setup-dependencies-native/action.yml b/.github/actions/setup-dependencies-native/action.yml index e52443772..f5d2f6b28 100644 --- a/.github/actions/setup-dependencies-native/action.yml +++ b/.github/actions/setup-dependencies-native/action.yml @@ -50,6 +50,8 @@ runs: gsettings-desktop-schemas libnotify4 libnotify-dev + clang-format + clang-tidy libwebkit2gtk-4.1-dev libgtk-3-dev libglib2.0-dev diff --git a/.github/scripts/native_quality.sh b/.github/scripts/native_quality.sh new file mode 100644 index 000000000..9a4863d37 --- /dev/null +++ b/.github/scripts/native_quality.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +native_root="${repo_root}/src/InfiniFrame.Native" +quality_root="${native_root}/build/native-quality" + +collect_format_sources() { + git -C "${repo_root}" ls-files --cached --others --exclude-standard -- \ + 'src/InfiniFrame.Native/*.cpp' \ + 'src/InfiniFrame.Native/*.h' \ + 'src/InfiniFrame.Native/*.mm' \ + 'src/InfiniFrame.Native/**/*.cpp' \ + 'src/InfiniFrame.Native/**/*.h' \ + 'src/InfiniFrame.Native/**/*.mm' | + grep -v '^src/InfiniFrame.Native/Dependencies/' | + sed "s#^#${repo_root}/#" +} + +run_format_check() { + mapfile -t sources < <(collect_format_sources) + + if [[ ${#sources[@]} -eq 0 ]]; then + echo "No native sources found for clang-format." + return 0 + fi + + clang-format --dry-run --Werror "${sources[@]}" +} + +configure_tidy_build() { + cmake -S "${native_root}" \ + -B "${quality_root}/tidy" \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DINFINIFRAME_BUILD_TEST_EXPORTS=ON +} + +collect_tidy_sources() { + local compile_commands="${quality_root}/tidy/compile_commands.json" + + python3 - "${compile_commands}" "${repo_root}" <<'PY' +import json +import pathlib +import sys + +compile_commands = pathlib.Path(sys.argv[1]) +repo_root = pathlib.Path(sys.argv[2]).resolve() +native_root = repo_root / "src" / "InfiniFrame.Native" +dependencies_root = native_root / "Dependencies" + +entries = json.loads(compile_commands.read_text(encoding="utf-8")) +seen = set() + +for entry in entries: + source = pathlib.Path(entry["file"]).resolve() + + if dependencies_root in source.parents: + continue + + if native_root not in (source, *source.parents): + continue + + if source.suffix not in {".cpp", ".mm"}: + continue + + if source in seen: + continue + + seen.add(source) + print(source) +PY +} + +run_clang_tidy() { + configure_tidy_build + mapfile -t sources < <(collect_tidy_sources) + + if [[ ${#sources[@]} -eq 0 ]]; then + echo "No native compile database sources found for clang-tidy." + return 0 + fi + + clang-tidy \ + -p "${quality_root}/tidy" \ + --checks='-*,clang-analyzer-*' \ + --warnings-as-errors='clang-analyzer-*' \ + "${sources[@]}" +} + +run_sanitizer_build() { + cmake -S "${native_root}" \ + -B "${quality_root}/sanitizer" \ + -DCMAKE_BUILD_TYPE=Debug \ + -DINFINIFRAME_BUILD_TEST_EXPORTS=ON + + cmake --build "${quality_root}/sanitizer" --parallel +} + +case "${1:-all}" in + format) + run_format_check + ;; + clang-tidy) + run_clang_tidy + ;; + sanitizer-build) + run_sanitizer_build + ;; + all) + run_format_check + run_clang_tidy + run_sanitizer_build + ;; + *) + echo "Usage: $0 [format|clang-tidy|sanitizer-build|all]" >&2 + exit 2 + ;; +esac diff --git a/.github/workflows/shared-testing-linux.yml b/.github/workflows/shared-testing-linux.yml index a85cc3dfb..f8af69c4c 100644 --- a/.github/workflows/shared-testing-linux.yml +++ b/.github/workflows/shared-testing-linux.yml @@ -84,6 +84,18 @@ jobs: brew-cache-key: ${{ matrix.os }}-${{ matrix.arch }}-brew-native-${{ hashFiles('.github/actions/setup-dependencies-native/action.yml', '.github/workflows/shared-testing-linux.yml') }} brew-restore-key: ${{ matrix.os }}-${{ matrix.arch }}-brew-native- + - name: Native Format Check + if: matrix.arch == 'x64' + run: bash .github/scripts/native_quality.sh format + + - name: Native Clang-Tidy + if: matrix.arch == 'x64' + run: bash .github/scripts/native_quality.sh clang-tidy + + - name: Native Sanitizer Build + if: matrix.arch == 'x64' + run: bash .github/scripts/native_quality.sh sanitizer-build + - name: Compile GSettings schemas run: sudo glib-compile-schemas /usr/share/glib-2.0/schemas/ @@ -96,11 +108,13 @@ jobs: --configuration Release \ --no-restore \ /p:SolutionDir="${{ github.workspace }}/" \ - /p:Platform=${{ matrix.arch }} + /p:Platform=${{ matrix.arch }} \ + /p:InfiniFrameNativeTestExports=true dotnet build InfiniFrame.GitHubActions.Testing.slnf \ --configuration Release \ - --no-restore + --no-restore \ + /p:InfiniFrameNativeTestExports=true - name: Verify Native Binaries uses: ./.github/actions/validate-native-test-binaries diff --git a/.github/workflows/shared-testing-macos.yml b/.github/workflows/shared-testing-macos.yml index 9eb5865f9..ecb7944fa 100644 --- a/.github/workflows/shared-testing-macos.yml +++ b/.github/workflows/shared-testing-macos.yml @@ -95,10 +95,11 @@ jobs: --configuration Release \ --no-restore \ /p:SolutionDir="${{ github.workspace }}/" \ - /p:Platform=${{ matrix.arch }} + /p:Platform=${{ matrix.arch }} \ + /p:InfiniFrameNativeTestExports=true - name: Build Release - run: dotnet build InfiniFrame.GitHubActions.Testing.slnf --configuration Release --no-restore + run: dotnet build InfiniFrame.GitHubActions.Testing.slnf --configuration Release --no-restore /p:InfiniFrameNativeTestExports=true - name: Verify Native Binaries uses: ./.github/actions/validate-native-test-binaries diff --git a/.github/workflows/shared-testing-windows.yml b/.github/workflows/shared-testing-windows.yml index ed5d4d7c2..08c8520c0 100644 --- a/.github/workflows/shared-testing-windows.yml +++ b/.github/workflows/shared-testing-windows.yml @@ -95,11 +95,13 @@ jobs: --configuration Release ` --no-restore ` -p:SolutionDir="${{ github.workspace }}/" ` - -p:Platform=${{ matrix.arch }} + -p:Platform=${{ matrix.arch }} ` + -p:InfiniFrameNativeTestExports=true dotnet build InfiniFrame.GitHubActions.Testing.slnf ` --configuration Release ` - --no-restore + --no-restore ` + -p:InfiniFrameNativeTestExports=true - name: Verify Native Binaries uses: ./.github/actions/validate-native-test-binaries diff --git a/src/InfiniFrame.Native/CMakeLists.txt b/src/InfiniFrame.Native/CMakeLists.txt index ab781cb40..fe819a8ae 100644 --- a/src/InfiniFrame.Native/CMakeLists.txt +++ b/src/InfiniFrame.Native/CMakeLists.txt @@ -11,6 +11,8 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +option(INFINIFRAME_BUILD_TEST_EXPORTS "Build test-only native exports into InfiniFrame.Native" OFF) + if (APPLE) set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "" FORCE) endif () @@ -31,21 +33,50 @@ infiniframe_setup_dependencies() # Source Files # ---------------------------------------------------------------------------------------------------------------------- set(COMMON_SOURCES - Exports.cpp + Interop/Exports.cpp + Interop/Exports.Browser.cpp + Interop/Exports.Dialog.cpp + Interop/Exports.Events.cpp + Interop/Exports.Lifecycle.cpp + Interop/Exports.Memory.cpp + Interop/Exports.Platform.cpp + Interop/Exports.WindowCommands.cpp ) set(TEST_SOURCES Exports.Tests.cpp ) +if (NOT INFINIFRAME_BUILD_TEST_EXPORTS) + set(TEST_SOURCES) +endif () + set(WINDOWS_SOURCES Platform/Windows/Window.cpp + Platform/Windows/Monitors.Win32.cpp + Platform/Windows/Notifications.WinToast.cpp + Platform/Windows/UiDispatcher.Win32.cpp + Platform/Windows/WindowProc.Win32.cpp + Platform/Windows/WebView2Bridge.Win32.cpp + Platform/Windows/WebView2CustomSchemes.Win32.cpp + Platform/Windows/WebView2Host.Win32.cpp + Platform/Windows/WebView2Messaging.Win32.cpp + Platform/Windows/WebView2ResourceRequests.Win32.cpp + Platform/Windows/WebView2Settings.Win32.cpp Platform/Windows/DarkMode.cpp Platform/Windows/Dialog.cpp ) set(LINUX_SOURCES Platform/Linux/Window.cpp + Platform/Linux/Monitors.Gtk.cpp + Platform/Linux/Notifications.LibNotify.cpp + Platform/Linux/UiDispatcher.Gtk.cpp + Platform/Linux/WindowState.Gtk.cpp + Platform/Linux/WebKitBridge.Gtk.cpp + Platform/Linux/WebKitCustomSchemes.Gtk.cpp + Platform/Linux/WebKitMessaging.Gtk.cpp + Platform/Linux/WebKitSettings.Gtk.cpp Platform/Linux/Dialog.cpp ) @@ -56,12 +87,26 @@ set(MAC_SOURCES Platform/Mac/NavigationDelegate.mm Platform/Mac/UrlSchemeHandler.mm Platform/Mac/NSWindowBorderless.mm + Platform/Mac/Monitors.Cocoa.mm + Platform/Mac/Notifications.UserNotifications.Cocoa.mm + Platform/Mac/UiDispatcher.Cocoa.mm + Platform/Mac/WKCustomSchemes.Cocoa.mm + Platform/Mac/WKJsInterop.Cocoa.mm + Platform/Mac/WKWebViewBridge.Cocoa.mm + Platform/Mac/WKWebViewSettings.Cocoa.mm + Platform/Mac/WindowState.Cocoa.mm Platform/Mac/Dialog.mm Platform/Mac/Window.mm ) set(HEADER_FILES Core/InfiniFrame.h + Interop/InitParamsReader.h + Interop/ExportApi.h + Interop/NativeBuffer.h + Interop/NativeResult.h + Interop/NativeString.h + Shared/CustomSchemeResponse.h Core/InfiniFrameWindow.h Core/InfiniFrameDialog.h Embedded/Embedded.h @@ -73,12 +118,18 @@ set(HEADER_FILES Utils/Common.h Utils/Event.h Platform/Windows/ToastHandler.h + Platform/Windows/WindowImpl.Win32.h + Platform/Windows/WindowProc.Win32.h + Platform/Windows/WebView2CustomSchemes.Win32.h + Platform/Linux/WindowImpl.Gtk.h + Platform/Linux/WebKitMessaging.Gtk.h Platform/Windows/DarkMode.h Platform/Mac/AppDelegate.h Platform/Mac/NavigationDelegate.h Platform/Mac/NSWindowBorderless.h Platform/Mac/UiDelegate.h Platform/Mac/WindowDelegate.h + Platform/Mac/WindowImpl.Cocoa.h Platform/Mac/UrlSchemeHandler.h ) @@ -96,6 +147,8 @@ if (WIN32) elseif (APPLE) infiniframe_configure_macos_target( ${PROJECT_NAME} + "${COMMON_SOURCES}" + "${TEST_SOURCES}" "${MAC_SOURCES}" "${HEADER_FILES}" ) diff --git a/src/InfiniFrame.Native/Exports.cpp b/src/InfiniFrame.Native/Exports.cpp deleted file mode 100644 index 742f7e80c..000000000 --- a/src/InfiniFrame.Native/Exports.cpp +++ /dev/null @@ -1,770 +0,0 @@ -#include "Core/InfiniFrame.h" -#ifdef __linux__ -#include -#endif - -#ifdef _WIN32 -#define EXPORTED __declspec(dllexport) -#else -#define EXPORTED -#endif - -/** - * @file Exports.cpp - * @brief C API for InfiniFrame native interop - * - * Memory management: - * - InfiniFrame_ctor returns ownership to caller (.NET side) - * - InfiniFrame_dtor transfers ownership back and destroys instance - * - All string returns (AutoString) must be freed with InfiniFrame_FreeString - * - * Thread safety: - * - All methods except Invoke must be called from UI thread - * - Invoke marshals calls to UI thread safely - */ - -extern "C" { -#ifdef _WIN32 - /** - * @brief Register InfiniFrame window class (Windows) - * @param hInstance Application instance handle - */ - EXPORTED void InfiniFrame_register_win32(const HINSTANCE hInstance) { - InfiniFrameWindow::Register(hInstance); - } - - /** - * @brief Get native window handle (Windows) - * @param instance InfiniFrame instance - * @return HWND window handle - */ - EXPORTED HWND InfiniFrame_getHwnd_win32(InfiniFrameWindow* instance) { - return instance->getHwnd(); - } - - /** - * @brief Set WebView2 runtime path (Windows) - * @param instance InfiniFrame instance - * @param webView2RuntimePath Path to WebView2 runtime - */ - EXPORTED void InfiniFrame_setWebView2RuntimePath_win32(InfiniFrameWindow*, const AutoString webView2RuntimePath) { - InfiniFrameWindow::SetWebView2RuntimePath(webView2RuntimePath); - } - - /** - * @brief Get notifications enabled status (Windows) - * @param instance InfiniFrame instance - * @param disabled Output: notifications disabled status - */ - EXPORTED void InfiniFrame_GetNotificationsEnabled(InfiniFrameWindow* instance, bool* disabled) { - instance->GetNotificationsEnabled(disabled); - } -#elif __APPLE__ - /** - * @brief Register InfiniFrame application (macOS) - */ - EXPORTED void InfiniFrame_register_mac() { - InfiniFrameWindow::Register(); - } -#endif - - /** - * @brief Create new InfiniFrame window instance - * @param initParams Initialization parameters - * @return Raw pointer - ownership transferred to caller (.NET) - */ - EXPORTED InfiniFrameWindow* InfiniFrame_ctor(InfiniFrameInitParams* initParams) { - auto instance = std::make_unique(initParams); - return instance.release(); - } - - /** - * @brief Destroy InfiniFrame window instance - * @param instance Raw pointer from InfiniFrame_ctor - */ - EXPORTED void InfiniFrame_dtor(InfiniFrameWindow* instance) { - if (instance != nullptr) { - std::unique_ptr guard{instance}; - } - } - - /** - * @brief Center window on screen - * @param instance InfiniFrame instance - */ - EXPORTED void InfiniFrame_Center(InfiniFrameWindow* instance) { - instance->Center(); - } - - /** - * @brief Clear browser auto-fill data - * @param instance InfiniFrame instance - */ - EXPORTED void InfiniFrame_ClearBrowserAutoFill(InfiniFrameWindow* instance) { - instance->ClearBrowserAutoFill(); - } - - /** - * @brief Close window - * @param instance InfiniFrame instance - */ - EXPORTED void InfiniFrame_Close(InfiniFrameWindow* instance) { - instance->Close(); - } - - /** - * @brief Get transparent enabled status - * @param instance InfiniFrame instance - * @param enabled Output: transparent enabled status - */ - EXPORTED void InfiniFrame_GetTransparentEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetTransparentEnabled(enabled); - } - - /** - * @brief Get context menu enabled status - * @param instance InfiniFrame instance - * @param enabled Output: context menu enabled status - */ - EXPORTED void InfiniFrame_GetContextMenuEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetContextMenuEnabled(enabled); - } - - /** - * @brief Get zoom enabled status - * @param instance InfiniFrame instance - * @param enabled Output: zoom enabled status - */ - EXPORTED void InfiniFrame_GetZoomEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetZoomEnabled(enabled); - } - - /** - * @brief Get dev tools enabled status - * @param instance InfiniFrame instance - * @param enabled Output: dev tools enabled status - */ - EXPORTED void InfiniFrame_GetDevToolsEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetDevToolsEnabled(enabled); - } - - /** - * @brief Get full screen status - * @param instance InfiniFrame instance - * @param fullScreen Output: full screen status - */ - EXPORTED void InfiniFrame_GetFullScreen(InfiniFrameWindow* instance, bool* fullScreen) { - instance->GetFullScreen(fullScreen); - } - - /** - * @brief Get grant browser permissions status - * @param instance InfiniFrame instance - * @param grant Output: grant browser permissions status - */ - EXPORTED void InfiniFrame_GetGrantBrowserPermissions(InfiniFrameWindow* instance, bool* grant) { - instance->GetGrantBrowserPermissions(grant); - } - - /** - * @brief Get user agent string - * @param instance InfiniFrame instance - * @return User agent string - */ - EXPORTED AutoString InfiniFrame_GetUserAgent(InfiniFrameWindow* instance) { - return instance->GetUserAgent(); - } - - /** - * @brief Get media autoplay enabled status - * @param instance InfiniFrame instance - * @param enabled Output: media autoplay enabled status - */ - EXPORTED void InfiniFrame_GetMediaAutoplayEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetMediaAutoplayEnabled(enabled); - } - - /** - * @brief Get file system access enabled status - * @param instance InfiniFrame instance - * @param enabled Output: file system access enabled status - */ - EXPORTED void InfiniFrame_GetFileSystemAccessEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetFileSystemAccessEnabled(enabled); - } - - /** - * @brief Get web security enabled status - * @param instance InfiniFrame instance - * @param enabled Output: web security enabled status - */ - EXPORTED void InfiniFrame_GetWebSecurityEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetWebSecurityEnabled(enabled); - } - - /** - * @brief Get JavaScript clipboard access enabled status - * @param instance InfiniFrame instance - * @param enabled Output: JavaScript clipboard access enabled status - */ - EXPORTED void InfiniFrame_GetJavascriptClipboardAccessEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetJavascriptClipboardAccessEnabled(enabled); - } - - /** - * @brief Get media stream enabled status - * @param instance InfiniFrame instance - * @param enabled Output: media stream enabled status - */ - EXPORTED void InfiniFrame_GetMediaStreamEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetMediaStreamEnabled(enabled); - } - - /** - * @brief Get smooth scrolling enabled status - * @param instance InfiniFrame instance - * @param enabled Output: smooth scrolling enabled status - */ - EXPORTED void InfiniFrame_GetSmoothScrollingEnabled(InfiniFrameWindow* instance, bool* enabled) { - instance->GetSmoothScrollingEnabled(enabled); - } - - /** - * @brief Get maximized status - * @param instance InfiniFrame instance - * @param isMaximized Output: maximized status - */ - EXPORTED void InfiniFrame_GetMaximized(InfiniFrameWindow* instance, bool* isMaximized) { - instance->GetMaximized(isMaximized); - } - - /** - * @brief Get minimized status - * @param instance InfiniFrame instance - * @param isMinimized Output: minimized status - */ - EXPORTED void InfiniFrame_GetMinimized(InfiniFrameWindow* instance, bool* isMinimized) { - instance->GetMinimized(isMinimized); - } - - /** - * @brief Get ignore certificate errors enabled status - * @param instance InfiniFrame instance - * @param disabled Output: ignore certificate errors enabled status - */ - EXPORTED void InfiniFrame_GetIgnoreCertificateErrorsEnabled(InfiniFrameWindow* instance, bool* disabled) { - instance->GetIgnoreCertificateErrorsEnabled(disabled); - } - - /** - * @brief Get window position - * @param instance InfiniFrame instance - * @param x Output: X coordinate - * @param y Output: Y coordinate - */ - EXPORTED void InfiniFrame_GetPosition(InfiniFrameWindow* instance, int* x, int* y) { - instance->GetPosition(x, y); - } - - /** - * @brief Get resizable status - * @param instance InfiniFrame instance - * @param resizable Output: resizable status - */ - EXPORTED void InfiniFrame_GetResizable(InfiniFrameWindow* instance, bool* resizable) { - instance->GetResizable(resizable); - } - - /** - * @brief Get screen DPI - * @param instance InfiniFrame instance - * @return Screen DPI value - */ - EXPORTED unsigned int InfiniFrame_GetScreenDpi(InfiniFrameWindow* instance) { - return instance->GetScreenDpi(); - } - - /** - * @brief Get window size - * @param instance InfiniFrame instance - * @param width Output: window width - * @param height Output: window height - */ - EXPORTED void InfiniFrame_GetSize(InfiniFrameWindow* instance, int* width, int* height) { - instance->GetSize(width, height); - } - - /** - * @brief Get the window maximum size constraints - * @param instance InfiniFrame instance - * @param width Output: maximum window width - * @param height Output: maximum window height - */ - EXPORTED void InfiniFrame_GetMaxSize(InfiniFrameWindow* instance, int* width, int* height) { - instance->GetMaxSize(width, height); - } - - /** - * @brief Get the window minimum size constraints - * @param instance InfiniFrame instance - * @param width Output: minimum window width - * @param height Output: minimum window height - */ - EXPORTED void InfiniFrame_GetMinSize(InfiniFrameWindow* instance, int* width, int* height) { - instance->GetMinSize(width, height); - } - - /** - * @brief Get window title - * @param instance InfiniFrame instance - * @return Window title string - */ - EXPORTED AutoString InfiniFrame_GetTitle(InfiniFrameWindow* instance) { - return instance->GetTitle(); - } - - /** - * @brief Get topmost status - * @param instance InfiniFrame instance - * @param topmost Output: topmost status - */ - EXPORTED void InfiniFrame_GetTopmost(InfiniFrameWindow* instance, bool* topmost) { - instance->GetTopmost(topmost); - } - - /** - * @brief Get zoom level - * @param instance InfiniFrame instance - * @param zoom Output: zoom level percentage - */ - EXPORTED void InfiniFrame_GetZoom(InfiniFrameWindow* instance, int* zoom) { - instance->GetZoom(zoom); - } - - /** - * @brief Get focused status - * @param instance InfiniFrame instance - * @param isFocused Output: focused status - */ - EXPORTED void InfiniFrame_GetFocused(InfiniFrameWindow* instance, bool* isFocused) { - instance->GetFocused(isFocused); - } - - /** - * @brief Get icon file name - * @param instance InfiniFrame instance - * @return Icon file name string - */ - EXPORTED AutoString InfiniFrame_GetIconFileName(InfiniFrameWindow* instance) { - return instance->GetIconFileName(); - } - - /** - * @brief Navigate to HTML string - * @param instance InfiniFrame instance - * @param content HTML content string - */ - EXPORTED void InfiniFrame_NavigateToString(InfiniFrameWindow* instance, const AutoString content) { - instance->NavigateToString(content); - } - - /** - * @brief Navigate to URL - * @param instance InfiniFrame instance - * @param url URL to navigate to - */ - EXPORTED void InfiniFrame_NavigateToUrl(InfiniFrameWindow* instance, const AutoString url) { - instance->NavigateToUrl(url); - } - - /** - * @brief Restore window from minimized/maximized state - * @param instance InfiniFrame instance - */ - EXPORTED void InfiniFrame_Restore(InfiniFrameWindow* instance) { - instance->Restore(); - } - - /** - * @brief Send message to WebView JavaScript - * @param instance InfiniFrame instance - * @param message Message string to send - */ - EXPORTED void InfiniFrame_SendWebMessage(InfiniFrameWindow* instance, const AutoString message) { - instance->SendWebMessage(message); - } - - /** - * @brief Set transparent enabled status - * @param instance InfiniFrame instance - * @param enabled Transparent enabled status - */ - EXPORTED void InfiniFrame_SetTransparentEnabled(InfiniFrameWindow* instance, const bool enabled) { - instance->SetTransparentEnabled(enabled); - } - - /** - * @brief Set context menu enabled status - * @param instance InfiniFrame instance - * @param enabled Context menu enabled status - */ - EXPORTED void InfiniFrame_SetContextMenuEnabled(InfiniFrameWindow* instance, const bool enabled) { - instance->SetContextMenuEnabled(enabled); - } - - /** - * @brief Set zoom enabled status - * @param instance InfiniFrame instance - * @param enabled Zoom enabled status - */ - EXPORTED void InfiniFrame_SetZoomEnabled(InfiniFrameWindow* instance, const bool enabled) { - instance->SetZoomEnabled(enabled); - } - - /** - * @brief Set dev tools enabled status - * @param instance InfiniFrame instance - * @param enabled Dev tools enabled status - */ - EXPORTED void InfiniFrame_SetDevToolsEnabled(InfiniFrameWindow* instance, const bool enabled) { - instance->SetDevToolsEnabled(enabled); - } - - /** - * @brief Set full screen status - * @param instance InfiniFrame instance - * @param fullScreen Full screen status - */ - EXPORTED void InfiniFrame_SetFullScreen(InfiniFrameWindow* instance, const bool fullScreen) { - instance->SetFullScreen(fullScreen); - } - - /** - * @brief Set window icon from file - * @param instance InfiniFrame instance - * @param filename Icon file path - */ - EXPORTED void InfiniFrame_SetIconFile(InfiniFrameWindow* instance, const AutoString filename) { - instance->SetIconFile(filename); - } - - /** - * @brief Set maximized status - * @param instance InfiniFrame instance - * @param maximized Maximized status - */ - EXPORTED void InfiniFrame_SetMaximized(InfiniFrameWindow* instance, const bool maximized) { - instance->SetMaximized(maximized); - } - - /** - * @brief Set maximum window size - * @param instance InfiniFrame instance - * @param width Maximum width - * @param height Maximum height - */ - EXPORTED void InfiniFrame_SetMaxSize(InfiniFrameWindow* instance, const int width, const int height) { - instance->SetMaxSize(width, height); - } - - /** - * @brief Set minimized status - * @param instance InfiniFrame instance - * @param minimized Minimized status - */ - EXPORTED void InfiniFrame_SetMinimized(InfiniFrameWindow* instance, const bool minimized) { - instance->SetMinimized(minimized); - } - - /** - * @brief Set minimum window size - * @param instance InfiniFrame instance - * @param width Minimum width - * @param height Minimum height - */ - EXPORTED void InfiniFrame_SetMinSize(InfiniFrameWindow* instance, const int width, const int height) { - instance->SetMinSize(width, height); - } - - /** - * @brief Set window position - * @param instance InfiniFrame instance - * @param x X coordinate - * @param y Y coordinate - */ - EXPORTED void InfiniFrame_SetPosition(InfiniFrameWindow* instance, const int x, const int y) { - instance->SetPosition(x, y); - } - - /** - * @brief Set resizable status - * @param instance InfiniFrame instance - * @param resizable Resizable status - */ - EXPORTED void InfiniFrame_SetResizable(InfiniFrameWindow* instance, const bool resizable) { - instance->SetResizable(resizable); - } - - /** - * @brief Set window size - * @param instance InfiniFrame instance - * @param width Window width - * @param height Window height - */ - EXPORTED void InfiniFrame_SetSize(InfiniFrameWindow* instance, const int width, const int height) { - instance->SetSize(width, height); - } - - /** - * @brief Set window title - * @param instance InfiniFrame instance - * @param title Window title string - */ - EXPORTED void InfiniFrame_SetTitle(InfiniFrameWindow* instance, const AutoString title) { - instance->SetTitle(title); - } - - /** - * @brief Set topmost status - * @param instance InfiniFrame instance - * @param topmost Topmost status - */ - EXPORTED void InfiniFrame_SetTopmost(InfiniFrameWindow* instance, const bool topmost) { - instance->SetTopmost(topmost); - } - - /** - * @brief Set zoom level - * @param instance InfiniFrame instance - * @param zoom Zoom level percentage - */ - EXPORTED void InfiniFrame_SetZoom(InfiniFrameWindow* instance, const int zoom) { - instance->SetZoom(zoom); - } - - /** - * @brief Show notification - * @param instance InfiniFrame instance - * @param title Notification title - * @param body Notification body - */ - EXPORTED void InfiniFrame_ShowNotification( - InfiniFrameWindow* instance, - const AutoString title, - const AutoString body - ) { - instance->ShowNotification(title, body); - } - - /** - * @brief Wait for window exit - * @param instance InfiniFrame instance - */ - EXPORTED void InfiniFrame_WaitForExit(InfiniFrameWindow* instance) { - instance->WaitForExit(); - } - - /** - * @brief Free string allocated by native code - * @param value String to free - */ - EXPORTED void InfiniFrame_FreeString(AutoString value) { - if (value == nullptr) - return; -#ifdef _WIN32 - delete[] value; -#elif __linux__ - g_free(value); -#elif __APPLE__ - free(value); -#else - free(value); -#endif - } - - /** - * @brief Free string array allocated by native code - * @param values String array to free - * @param count Number of strings in array - */ - EXPORTED void InfiniFrame_FreeStringArray(AutoString* values, const int count) { - if (values == nullptr) - return; - - for (int i = 0; i < count; ++i) { - InfiniFrame_FreeString(values[i]); - } - -#ifdef _WIN32 - delete[] values; -#elif __linux__ - delete[] values; -#elif __APPLE__ - free(values); -#else - free(values); -#endif - } - - /** - * @brief Show open file dialog - * @param inst InfiniFrame instance - * @param title Dialog title - * @param defaultPath Default path - * @param multiSelect Allow multiple selection - * @param filters File filters - * @param filterCount Number of filters - * @param resultCount Output: number of selected files - * @return Array of selected file paths - */ - EXPORTED AutoString* InfiniFrame_ShowOpenFile( - InfiniFrameWindow* inst, - const AutoString title, - const AutoString defaultPath, - const bool multiSelect, - AutoString* filters, - const int filterCount, - int* resultCount - ) { - return inst->GetDialog()->ShowOpenFile(title, defaultPath, multiSelect, filters, filterCount, resultCount); - } - - /** - * @brief Show open folder dialog - * @param inst InfiniFrame instance - * @param title Dialog title - * @param defaultPath Default path - * @param multiSelect Allow multiple selection - * @param resultCount Output: number of selected folders - * @return Array of selected folder paths - */ - EXPORTED AutoString* InfiniFrame_ShowOpenFolder( - InfiniFrameWindow* inst, - const AutoString title, - const AutoString defaultPath, - const bool multiSelect, - int* resultCount - ) { - return inst->GetDialog()->ShowOpenFolder(title, defaultPath, multiSelect, resultCount); - } - - /** - * @brief Show save file dialog - * @param inst InfiniFrame instance - * @param title Dialog title - * @param defaultPath Default path - * @param filters File filters - * @param filterCount Number of filters - * @param defaultFileName Default file name - * @return Selected file path - */ - EXPORTED AutoString InfiniFrame_ShowSaveFile( - InfiniFrameWindow* inst, - const AutoString title, - const AutoString defaultPath, - AutoString* filters, - const int filterCount, - const AutoString defaultFileName - ) { - return inst->GetDialog()->ShowSaveFile(title, defaultPath, filters, filterCount, defaultFileName); - } - - /** - * @brief Show message dialog - * @param inst InfiniFrame instance - * @param title Dialog title - * @param text Message text - * @param buttons Button configuration - * @param icon Icon type - * @return User response - */ - EXPORTED DialogResult InfiniFrame_ShowMessage( - InfiniFrameWindow* inst, - const AutoString title, - const AutoString text, - const DialogButtons buttons, - const DialogIcon icon - ) { - return inst->GetDialog()->ShowMessage(title, text, buttons, icon); - } - - /** - * @brief Add custom scheme name - * @param instance InfiniFrame instance - * @param scheme Scheme name to add - */ - EXPORTED void InfiniFrame_AddCustomSchemeName(InfiniFrameWindow* instance, const AutoString scheme) { - instance->AddCustomSchemeName(scheme); - } - - /** - * @brief Get all monitors - * @param instance InfiniFrame instance - * @param callback Callback function to receive monitor info - */ - EXPORTED void InfiniFrame_GetAllMonitors(InfiniFrameWindow* instance, const GetAllMonitorsCallback callback) { - instance->GetAllMonitors(callback); - } - - /** - * @brief Set closing callback - * @param instance InfiniFrame instance - * @param callback Closing callback - */ - EXPORTED void InfiniFrame_SetClosingCallback(InfiniFrameWindow* instance, const ClosingCallback callback) { - instance->SetClosingCallback(callback); - } - - /** - * @brief Set focus-in callback - * @param instance InfiniFrame instance - * @param callback Focus-in callback - */ - EXPORTED void InfiniFrame_SetFocusInCallback(InfiniFrameWindow* instance, const FocusInCallback callback) { - instance->SetFocusInCallback(callback); - } - - /** - * @brief Set focus-out callback - * @param instance InfiniFrame instance - * @param callback Focus-out callback - */ - EXPORTED void InfiniFrame_SetFocusOutCallback(InfiniFrameWindow* instance, const FocusOutCallback callback) { - instance->SetFocusOutCallback(callback); - } - - /** - * @brief Set moved callback - * @param instance InfiniFrame instance - * @param callback Moved callback - */ - EXPORTED void InfiniFrame_SetMovedCallback(InfiniFrameWindow* instance, const MovedCallback callback) { - instance->SetMovedCallback(callback); - } - - /** - * @brief Set resized callback - * @param instance InfiniFrame instance - * @param callback Resized callback - */ - EXPORTED void InfiniFrame_SetResizedCallback(InfiniFrameWindow* instance, const ResizedCallback callback) { - instance->SetResizedCallback(callback); - } - - /** - * @brief Invoke callback on UI thread - * @param instance InfiniFrame instance - * @param callback Callback to invoke - */ - EXPORTED void InfiniFrame_Invoke(InfiniFrameWindow* instance, const ACTION callback) { - instance->Invoke(callback); - } - - /** - * @brief Set window focused - * @param instance InfiniFrame instance - */ - EXPORTED void InfiniFrame_SetFocused(InfiniFrameWindow* instance) { - instance->SetFocused(); - } -} diff --git a/src/InfiniFrame.Native/InfiniFrame.Native.proj b/src/InfiniFrame.Native/InfiniFrame.Native.proj index efca56857..d4a210738 100644 --- a/src/InfiniFrame.Native/InfiniFrame.Native.proj +++ b/src/InfiniFrame.Native/InfiniFrame.Native.proj @@ -27,6 +27,9 @@ windows linux osx + false + ON + OFF $(SolutionDir)artifacts/native/$(CMakeOSDir)/$(CMakeArch)/$(Configuration) @@ -53,15 +56,15 @@ - + - + - + diff --git a/src/InfiniFrame.Native/Interop/ExportApi.h b/src/InfiniFrame.Native/Interop/ExportApi.h new file mode 100644 index 000000000..a0d3d0876 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/ExportApi.h @@ -0,0 +1,20 @@ +#pragma once +/** + * @file ExportApi.h + * @brief Shared declarations for InfiniFrame C ABI export translation units. + */ + +#ifndef INFINIFRAME_INTEROP_EXPORTAPI_H +#define INFINIFRAME_INTEROP_EXPORTAPI_H + +#include "Core/InfiniFrame.h" +#include "Interop/NativeResult.h" +#include "Interop/NativeString.h" + +#ifdef _WIN32 +#define INFINIFRAME_NATIVE_EXPORT __declspec(dllexport) +#else +#define INFINIFRAME_NATIVE_EXPORT +#endif + +#endif // INFINIFRAME_INTEROP_EXPORTAPI_H diff --git a/src/InfiniFrame.Native/Interop/Exports.Browser.cpp b/src/InfiniFrame.Native/Interop/Exports.Browser.cpp new file mode 100644 index 000000000..c5bd74330 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/Exports.Browser.cpp @@ -0,0 +1,87 @@ +#include "Interop/ExportApi.h" + +using namespace InfiniFrame::Native::Interop; + +extern "C" { + /** + * @brief Clear browser auto-fill data + * @param instance InfiniFrame instance + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_ClearBrowserAutoFill(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow& window) { + window.ClearBrowserAutoFill(); + }); + } + + /** + * @brief Navigate to HTML string + * @param instance InfiniFrame instance + * @param content HTML content string + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_NavigateToString( + InfiniFrameWindow* instance, + const AutoString content + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.NavigateToString(content); + }); + } + + /** + * @brief Navigate to URL + * @param instance InfiniFrame instance + * @param url URL to navigate to + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_NavigateToUrl( + InfiniFrameWindow* instance, + const AutoString url + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.NavigateToUrl(url); + }); + } + + /** + * @brief Send message to WebView JavaScript + * @param instance InfiniFrame instance + * @param message Message string to send + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_SendWebMessage( + InfiniFrameWindow* instance, + const AutoString message + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SendWebMessage(message); + }); + } + + /** + * @brief Show notification + * @param instance InfiniFrame instance + * @param title Notification title + * @param body Notification body + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_ShowNotification( + InfiniFrameWindow* instance, + const AutoString title, + const AutoString body + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.ShowNotification(title, body); + }); + } + + /** + * @brief Add custom scheme name + * @param instance InfiniFrame instance + * @param scheme Scheme name to add + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_AddCustomSchemeName( + InfiniFrameWindow* instance, + const AutoString scheme + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.AddCustomSchemeName(scheme); + }); + } +} diff --git a/src/InfiniFrame.Native/Interop/Exports.Dialog.cpp b/src/InfiniFrame.Native/Interop/Exports.Dialog.cpp new file mode 100644 index 000000000..36c691500 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/Exports.Dialog.cpp @@ -0,0 +1,109 @@ +#include "Interop/ExportApi.h" + +using namespace InfiniFrame::Native::Interop; + +extern "C" { + /** + * @brief Show open file dialog + * @param inst InfiniFrame instance + * @param title Dialog title + * @param defaultPath Default path + * @param multiSelect Allow multiple selection + * @param filters File filters + * @param filterCount Number of filters + * @param resultCount Output: number of selected files + * @return Array of selected file paths + */ + INFINIFRAME_NATIVE_EXPORT AutoString* InfiniFrame_ShowOpenFile( + InfiniFrameWindow* inst, + const AutoString title, + const AutoString defaultPath, + const bool multiSelect, + AutoString* filters, + const int filterCount, + int* resultCount + ) { + return RunWindowReturnExport( + inst, + static_cast(nullptr), + [=](InfiniFrameWindow& window) { + return window.GetDialog()->ShowOpenFile(title, defaultPath, multiSelect, filters, filterCount, resultCount); + }, + resultCount + ); + } + + /** + * @brief Show open folder dialog + * @param inst InfiniFrame instance + * @param title Dialog title + * @param defaultPath Default path + * @param multiSelect Allow multiple selection + * @param resultCount Output: number of selected folders + * @return Array of selected folder paths + */ + INFINIFRAME_NATIVE_EXPORT AutoString* InfiniFrame_ShowOpenFolder( + InfiniFrameWindow* inst, + const AutoString title, + const AutoString defaultPath, + const bool multiSelect, + int* resultCount + ) { + return RunWindowReturnExport( + inst, + static_cast(nullptr), + [=](InfiniFrameWindow& window) { + return window.GetDialog()->ShowOpenFolder(title, defaultPath, multiSelect, resultCount); + }, + resultCount + ); + } + + /** + * @brief Show save file dialog + * @param inst InfiniFrame instance + * @param title Dialog title + * @param defaultPath Default path + * @param filters File filters + * @param filterCount Number of filters + * @param defaultFileName Default file name + * @return Selected file path + */ + INFINIFRAME_NATIVE_EXPORT AutoString InfiniFrame_ShowSaveFile( + InfiniFrameWindow* inst, + const AutoString title, + const AutoString defaultPath, + AutoString* filters, + const int filterCount, + const AutoString defaultFileName + ) { + return RunWindowReturnExport( + inst, + static_cast(nullptr), + [=](InfiniFrameWindow& window) { + return window.GetDialog()->ShowSaveFile(title, defaultPath, filters, filterCount, defaultFileName); + } + ); + } + + /** + * @brief Show message dialog + * @param inst InfiniFrame instance + * @param title Dialog title + * @param text Message text + * @param buttons Button configuration + * @param icon Icon type + * @return User response + */ + INFINIFRAME_NATIVE_EXPORT DialogResult InfiniFrame_ShowMessage( + InfiniFrameWindow* inst, + const AutoString title, + const AutoString text, + const DialogButtons buttons, + const DialogIcon icon + ) { + return RunWindowReturnExport(inst, DialogResult::Cancel, [=](InfiniFrameWindow& window) { + return window.GetDialog()->ShowMessage(title, text, buttons, icon); + }); + } +} diff --git a/src/InfiniFrame.Native/Interop/Exports.Events.cpp b/src/InfiniFrame.Native/Interop/Exports.Events.cpp new file mode 100644 index 000000000..182ef58e8 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/Exports.Events.cpp @@ -0,0 +1,100 @@ +#include "Interop/ExportApi.h" + +using namespace InfiniFrame::Native::Interop; + +extern "C" { + /** + * @brief Get all monitors + * @param instance InfiniFrame instance + * @param callback Callback function to receive monitor info + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_GetAllMonitors( + InfiniFrameWindow* instance, + const GetAllMonitorsCallback callback + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetAllMonitors(callback); + }); + } + + /** + * @brief Set closing callback + * @param instance InfiniFrame instance + * @param callback Closing callback + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_SetClosingCallback( + InfiniFrameWindow* instance, + const ClosingCallback callback + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetClosingCallback(callback); + }); + } + + /** + * @brief Set focus-in callback + * @param instance InfiniFrame instance + * @param callback Focus-in callback + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_SetFocusInCallback( + InfiniFrameWindow* instance, + const FocusInCallback callback + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetFocusInCallback(callback); + }); + } + + /** + * @brief Set focus-out callback + * @param instance InfiniFrame instance + * @param callback Focus-out callback + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_SetFocusOutCallback( + InfiniFrameWindow* instance, + const FocusOutCallback callback + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetFocusOutCallback(callback); + }); + } + + /** + * @brief Set moved callback + * @param instance InfiniFrame instance + * @param callback Moved callback + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_SetMovedCallback( + InfiniFrameWindow* instance, + const MovedCallback callback + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetMovedCallback(callback); + }); + } + + /** + * @brief Set resized callback + * @param instance InfiniFrame instance + * @param callback Resized callback + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_SetResizedCallback( + InfiniFrameWindow* instance, + const ResizedCallback callback + ) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetResizedCallback(callback); + }); + } + + /** + * @brief Invoke callback on UI thread + * @param instance InfiniFrame instance + * @param callback Callback to invoke + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_Invoke(InfiniFrameWindow* instance, const ACTION callback) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.Invoke(callback); + }); + } +} diff --git a/src/InfiniFrame.Native/Interop/Exports.Lifecycle.cpp b/src/InfiniFrame.Native/Interop/Exports.Lifecycle.cpp new file mode 100644 index 000000000..500d250ec --- /dev/null +++ b/src/InfiniFrame.Native/Interop/Exports.Lifecycle.cpp @@ -0,0 +1,38 @@ +#include "Interop/ExportApi.h" + +#include + +using namespace InfiniFrame::Native::Interop; + +extern "C" { + /** + * @brief Create new InfiniFrame window instance + * @param initParams Initialization parameters + * @return Raw pointer - ownership transferred to caller (.NET) + */ + INFINIFRAME_NATIVE_EXPORT InfiniFrameWindow* InfiniFrame_ctor(InfiniFrameInitParams* initParams) { + if (initParams == nullptr) { + SetExportInvalidArgument(); + return nullptr; + } + + return RunReturnExport(static_cast(nullptr), [&] { + auto instance = std::make_unique(initParams); + return instance.release(); + }); + } + + /** + * @brief Destroy InfiniFrame window instance + * @param instance Raw pointer from InfiniFrame_ctor + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_dtor(InfiniFrameWindow* instance) { + if (instance == nullptr) { + return SetExportSuccess(); + } + + return RunExportStatus([&] { + std::unique_ptr guard{instance}; + }); + } +} diff --git a/src/InfiniFrame.Native/Interop/Exports.Memory.cpp b/src/InfiniFrame.Native/Interop/Exports.Memory.cpp new file mode 100644 index 000000000..76dd571da --- /dev/null +++ b/src/InfiniFrame.Native/Interop/Exports.Memory.cpp @@ -0,0 +1,51 @@ +#include "Interop/ExportApi.h" + +using namespace InfiniFrame::Native::Interop; + +extern "C" { + /** + * @brief Get and clear the latest native error message for the current thread. + * @return Error message string, or null when no message is available. + */ + INFINIFRAME_NATIVE_EXPORT AutoString InfiniFrame_GetLastErrorMessage() { + return RunReturnExport(static_cast(nullptr), [] { + const NativeString& message = GetExportErrorMessage(); + if (message.empty()) + return static_cast(nullptr); + + return AllocateStringCopy(message); + }); + } + + /** + * @brief Free string allocated by native code + * @param value String to free + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_FreeString(AutoString value) { + if (value == nullptr) { + return SetExportSuccess(); + } + + return RunExportStatus([&] { + FreeNativeString(value); + }); + } + + /** + * @brief Free string array allocated by native code + * @param values String array to free + * @param count Number of strings in array + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_FreeStringArray(AutoString* values, const int count) { + if (values == nullptr) { + return SetExportSuccess(); + } + + if (count < 0) + return SetExportInvalidArgument(); + + return RunExportStatus([&] { + FreeNativeStringArray(values, count); + }); + } +} diff --git a/src/InfiniFrame.Native/Interop/Exports.Platform.cpp b/src/InfiniFrame.Native/Interop/Exports.Platform.cpp new file mode 100644 index 000000000..7554ec0b6 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/Exports.Platform.cpp @@ -0,0 +1,62 @@ +#include "Interop/ExportApi.h" + +using namespace InfiniFrame::Native::Interop; + +extern "C" { +#ifdef _WIN32 + /** + * @brief Register InfiniFrame window class (Windows) + * @param hInstance Application instance handle + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_register_win32(const HINSTANCE hInstance) { + return RunExportStatus([&] { + InfiniFrameWindow::Register(hInstance); + }); + } + + /** + * @brief Get native window handle (Windows) + * @param instance InfiniFrame instance + * @return HWND window handle + */ + INFINIFRAME_NATIVE_EXPORT HWND InfiniFrame_getHwnd_win32(InfiniFrameWindow* instance) { + return RunWindowReturnExport(instance, static_cast(nullptr), [](InfiniFrameWindow& window) { + return window.getHwnd(); + }); + } + + /** + * @brief Set WebView2 runtime path (Windows) + * @param instance InfiniFrame instance + * @param webView2RuntimePath Path to WebView2 runtime + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_setWebView2RuntimePath_win32( + InfiniFrameWindow*, + const AutoString webView2RuntimePath + ) { + return RunExportStatus([&] { + InfiniFrameWindow::SetWebView2RuntimePath(webView2RuntimePath); + }); + } + + /** + * @brief Get notifications enabled status (Windows) + * @param instance InfiniFrame instance + * @param disabled Output: notifications disabled status + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_GetNotificationsEnabled(InfiniFrameWindow* instance, bool* disabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetNotificationsEnabled(disabled); + }, disabled); + } +#elif __APPLE__ + /** + * @brief Register InfiniFrame application (macOS) + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_register_mac() { + return RunExportStatus([] { + InfiniFrameWindow::Register(); + }); + } +#endif +} diff --git a/src/InfiniFrame.Native/Interop/Exports.WindowCommands.cpp b/src/InfiniFrame.Native/Interop/Exports.WindowCommands.cpp new file mode 100644 index 000000000..c5761b539 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/Exports.WindowCommands.cpp @@ -0,0 +1,55 @@ +#include "Interop/ExportApi.h" + +using namespace InfiniFrame::Native::Interop; + +extern "C" { + /** + * @brief Center window on screen + * @param instance InfiniFrame instance + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_Center(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow& window) { + window.Center(); + }); + } + + /** + * @brief Close window + * @param instance InfiniFrame instance + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_Close(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow& window) { + window.Close(); + }); + } + + /** + * @brief Restore window from minimized/maximized state + * @param instance InfiniFrame instance + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_Restore(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow& window) { + window.Restore(); + }); + } + + /** + * @brief Wait for window exit + * @param instance InfiniFrame instance + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_WaitForExit(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow& window) { + window.WaitForExit(); + }); + } + + /** + * @brief Set window focused + * @param instance InfiniFrame instance + */ + INFINIFRAME_NATIVE_EXPORT NativeStatusCode InfiniFrame_SetFocused(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow& window) { + window.SetFocused(); + }); + } +} diff --git a/src/InfiniFrame.Native/Interop/Exports.cpp b/src/InfiniFrame.Native/Interop/Exports.cpp new file mode 100644 index 000000000..e93c1e0eb --- /dev/null +++ b/src/InfiniFrame.Native/Interop/Exports.cpp @@ -0,0 +1,503 @@ +#include "Interop/ExportApi.h" + +#define EXPORTED INFINIFRAME_NATIVE_EXPORT + +using namespace InfiniFrame::Native::Interop; + +/** + * @file Exports.cpp + * @brief C API for InfiniFrame native interop + * + * Memory management: + * - InfiniFrame_ctor returns ownership to caller (.NET side) + * - InfiniFrame_dtor transfers ownership back and destroys instance + * - All string returns (AutoString) must be freed with InfiniFrame_FreeString + * + * Thread safety: + * - All methods except Invoke must be called from UI thread + * - Invoke marshals calls to UI thread safely + */ + +extern "C" { + /** + * @brief Get transparent enabled status + * @param instance InfiniFrame instance + * @param enabled Output: transparent enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetTransparentEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetTransparentEnabled(enabled); + }, enabled); + } + + /** + * @brief Get context menu enabled status + * @param instance InfiniFrame instance + * @param enabled Output: context menu enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetContextMenuEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetContextMenuEnabled(enabled); + }, enabled); + } + + /** + * @brief Get zoom enabled status + * @param instance InfiniFrame instance + * @param enabled Output: zoom enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetZoomEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetZoomEnabled(enabled); + }, enabled); + } + + /** + * @brief Get dev tools enabled status + * @param instance InfiniFrame instance + * @param enabled Output: dev tools enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetDevToolsEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetDevToolsEnabled(enabled); + }, enabled); + } + + /** + * @brief Get full screen status + * @param instance InfiniFrame instance + * @param fullScreen Output: full screen status + */ + EXPORTED NativeStatusCode InfiniFrame_GetFullScreen(InfiniFrameWindow* instance, bool* fullScreen) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetFullScreen(fullScreen); + }, fullScreen); + } + + /** + * @brief Get grant browser permissions status + * @param instance InfiniFrame instance + * @param grant Output: grant browser permissions status + */ + EXPORTED NativeStatusCode InfiniFrame_GetGrantBrowserPermissions(InfiniFrameWindow* instance, bool* grant) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetGrantBrowserPermissions(grant); + }, grant); + } + + /** + * @brief Get user agent string + * @param instance InfiniFrame instance + * @return User agent string + */ + EXPORTED AutoString InfiniFrame_GetUserAgent(InfiniFrameWindow* instance) { + return RunWindowReturnExport(instance, static_cast(nullptr), [](InfiniFrameWindow& window) { + return window.GetUserAgent(); + }); + } + + /** + * @brief Get media autoplay enabled status + * @param instance InfiniFrame instance + * @param enabled Output: media autoplay enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetMediaAutoplayEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetMediaAutoplayEnabled(enabled); + }, enabled); + } + + /** + * @brief Get file system access enabled status + * @param instance InfiniFrame instance + * @param enabled Output: file system access enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetFileSystemAccessEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetFileSystemAccessEnabled(enabled); + }, enabled); + } + + /** + * @brief Get web security enabled status + * @param instance InfiniFrame instance + * @param enabled Output: web security enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetWebSecurityEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetWebSecurityEnabled(enabled); + }, enabled); + } + + /** + * @brief Get JavaScript clipboard access enabled status + * @param instance InfiniFrame instance + * @param enabled Output: JavaScript clipboard access enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetJavascriptClipboardAccessEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetJavascriptClipboardAccessEnabled(enabled); + }, enabled); + } + + /** + * @brief Get media stream enabled status + * @param instance InfiniFrame instance + * @param enabled Output: media stream enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetMediaStreamEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetMediaStreamEnabled(enabled); + }, enabled); + } + + /** + * @brief Get smooth scrolling enabled status + * @param instance InfiniFrame instance + * @param enabled Output: smooth scrolling enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetSmoothScrollingEnabled(InfiniFrameWindow* instance, bool* enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetSmoothScrollingEnabled(enabled); + }, enabled); + } + + /** + * @brief Get maximized status + * @param instance InfiniFrame instance + * @param isMaximized Output: maximized status + */ + EXPORTED NativeStatusCode InfiniFrame_GetMaximized(InfiniFrameWindow* instance, bool* isMaximized) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetMaximized(isMaximized); + }, isMaximized); + } + + /** + * @brief Get minimized status + * @param instance InfiniFrame instance + * @param isMinimized Output: minimized status + */ + EXPORTED NativeStatusCode InfiniFrame_GetMinimized(InfiniFrameWindow* instance, bool* isMinimized) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetMinimized(isMinimized); + }, isMinimized); + } + + /** + * @brief Get ignore certificate errors enabled status + * @param instance InfiniFrame instance + * @param disabled Output: ignore certificate errors enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_GetIgnoreCertificateErrorsEnabled(InfiniFrameWindow* instance, bool* disabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetIgnoreCertificateErrorsEnabled(disabled); + }, disabled); + } + + /** + * @brief Get window position + * @param instance InfiniFrame instance + * @param x Output: X coordinate + * @param y Output: Y coordinate + */ + EXPORTED NativeStatusCode InfiniFrame_GetPosition(InfiniFrameWindow* instance, int* x, int* y) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetPosition(x, y); + }, x, y); + } + + /** + * @brief Get resizable status + * @param instance InfiniFrame instance + * @param resizable Output: resizable status + */ + EXPORTED NativeStatusCode InfiniFrame_GetResizable(InfiniFrameWindow* instance, bool* resizable) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetResizable(resizable); + }, resizable); + } + + /** + * @brief Get screen DPI + * @param instance InfiniFrame instance + * @return Screen DPI value + */ + EXPORTED unsigned int InfiniFrame_GetScreenDpi(InfiniFrameWindow* instance) { + return RunWindowReturnExport(instance, 0u, [](InfiniFrameWindow& window) { + return window.GetScreenDpi(); + }); + } + + /** + * @brief Get window size + * @param instance InfiniFrame instance + * @param width Output: window width + * @param height Output: window height + */ + EXPORTED NativeStatusCode InfiniFrame_GetSize(InfiniFrameWindow* instance, int* width, int* height) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetSize(width, height); + }, width, height); + } + + /** + * @brief Get the window maximum size constraints + * @param instance InfiniFrame instance + * @param width Output: maximum window width + * @param height Output: maximum window height + */ + EXPORTED NativeStatusCode InfiniFrame_GetMaxSize(InfiniFrameWindow* instance, int* width, int* height) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetMaxSize(width, height); + }, width, height); + } + + /** + * @brief Get the window minimum size constraints + * @param instance InfiniFrame instance + * @param width Output: minimum window width + * @param height Output: minimum window height + */ + EXPORTED NativeStatusCode InfiniFrame_GetMinSize(InfiniFrameWindow* instance, int* width, int* height) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetMinSize(width, height); + }, width, height); + } + + /** + * @brief Get window title + * @param instance InfiniFrame instance + * @return Window title string + */ + EXPORTED AutoString InfiniFrame_GetTitle(InfiniFrameWindow* instance) { + return RunWindowReturnExport(instance, static_cast(nullptr), [](InfiniFrameWindow& window) { + return window.GetTitle(); + }); + } + + /** + * @brief Get topmost status + * @param instance InfiniFrame instance + * @param topmost Output: topmost status + */ + EXPORTED NativeStatusCode InfiniFrame_GetTopmost(InfiniFrameWindow* instance, bool* topmost) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetTopmost(topmost); + }, topmost); + } + + /** + * @brief Get zoom level + * @param instance InfiniFrame instance + * @param zoom Output: zoom level percentage + */ + EXPORTED NativeStatusCode InfiniFrame_GetZoom(InfiniFrameWindow* instance, int* zoom) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetZoom(zoom); + }, zoom); + } + + /** + * @brief Get focused status + * @param instance InfiniFrame instance + * @param isFocused Output: focused status + */ + EXPORTED NativeStatusCode InfiniFrame_GetFocused(InfiniFrameWindow* instance, bool* isFocused) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.GetFocused(isFocused); + }, isFocused); + } + + /** + * @brief Get icon file name + * @param instance InfiniFrame instance + * @return Icon file name string + */ + EXPORTED AutoString InfiniFrame_GetIconFileName(InfiniFrameWindow* instance) { + return RunWindowReturnExport(instance, static_cast(nullptr), [](InfiniFrameWindow& window) { + return window.GetIconFileName(); + }); + } + + /** + * @brief Set transparent enabled status + * @param instance InfiniFrame instance + * @param enabled Transparent enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_SetTransparentEnabled(InfiniFrameWindow* instance, const bool enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetTransparentEnabled(enabled); + }); + } + + /** + * @brief Set context menu enabled status + * @param instance InfiniFrame instance + * @param enabled Context menu enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_SetContextMenuEnabled(InfiniFrameWindow* instance, const bool enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetContextMenuEnabled(enabled); + }); + } + + /** + * @brief Set zoom enabled status + * @param instance InfiniFrame instance + * @param enabled Zoom enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_SetZoomEnabled(InfiniFrameWindow* instance, const bool enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetZoomEnabled(enabled); + }); + } + + /** + * @brief Set dev tools enabled status + * @param instance InfiniFrame instance + * @param enabled Dev tools enabled status + */ + EXPORTED NativeStatusCode InfiniFrame_SetDevToolsEnabled(InfiniFrameWindow* instance, const bool enabled) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetDevToolsEnabled(enabled); + }); + } + + /** + * @brief Set full screen status + * @param instance InfiniFrame instance + * @param fullScreen Full screen status + */ + EXPORTED NativeStatusCode InfiniFrame_SetFullScreen(InfiniFrameWindow* instance, const bool fullScreen) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetFullScreen(fullScreen); + }); + } + + /** + * @brief Set window icon from file + * @param instance InfiniFrame instance + * @param filename Icon file path + */ + EXPORTED NativeStatusCode InfiniFrame_SetIconFile(InfiniFrameWindow* instance, const AutoString filename) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetIconFile(filename); + }); + } + + /** + * @brief Set maximized status + * @param instance InfiniFrame instance + * @param maximized Maximized status + */ + EXPORTED NativeStatusCode InfiniFrame_SetMaximized(InfiniFrameWindow* instance, const bool maximized) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetMaximized(maximized); + }); + } + + /** + * @brief Set maximum window size + * @param instance InfiniFrame instance + * @param width Maximum width + * @param height Maximum height + */ + EXPORTED NativeStatusCode InfiniFrame_SetMaxSize(InfiniFrameWindow* instance, const int width, const int height) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetMaxSize(width, height); + }); + } + + /** + * @brief Set minimized status + * @param instance InfiniFrame instance + * @param minimized Minimized status + */ + EXPORTED NativeStatusCode InfiniFrame_SetMinimized(InfiniFrameWindow* instance, const bool minimized) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetMinimized(minimized); + }); + } + + /** + * @brief Set minimum window size + * @param instance InfiniFrame instance + * @param width Minimum width + * @param height Minimum height + */ + EXPORTED NativeStatusCode InfiniFrame_SetMinSize(InfiniFrameWindow* instance, const int width, const int height) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetMinSize(width, height); + }); + } + + /** + * @brief Set window position + * @param instance InfiniFrame instance + * @param x X coordinate + * @param y Y coordinate + */ + EXPORTED NativeStatusCode InfiniFrame_SetPosition(InfiniFrameWindow* instance, const int x, const int y) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetPosition(x, y); + }); + } + + /** + * @brief Set resizable status + * @param instance InfiniFrame instance + * @param resizable Resizable status + */ + EXPORTED NativeStatusCode InfiniFrame_SetResizable(InfiniFrameWindow* instance, const bool resizable) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetResizable(resizable); + }); + } + + /** + * @brief Set window size + * @param instance InfiniFrame instance + * @param width Window width + * @param height Window height + */ + EXPORTED NativeStatusCode InfiniFrame_SetSize(InfiniFrameWindow* instance, const int width, const int height) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetSize(width, height); + }); + } + + /** + * @brief Set window title + * @param instance InfiniFrame instance + * @param title Window title string + */ + EXPORTED NativeStatusCode InfiniFrame_SetTitle(InfiniFrameWindow* instance, const AutoString title) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetTitle(title); + }); + } + + /** + * @brief Set topmost status + * @param instance InfiniFrame instance + * @param topmost Topmost status + */ + EXPORTED NativeStatusCode InfiniFrame_SetTopmost(InfiniFrameWindow* instance, const bool topmost) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetTopmost(topmost); + }); + } + + /** + * @brief Set zoom level + * @param instance InfiniFrame instance + * @param zoom Zoom level percentage + */ + EXPORTED NativeStatusCode InfiniFrame_SetZoom(InfiniFrameWindow* instance, const int zoom) { + return RunWindowExportStatus(instance, [=](InfiniFrameWindow& window) { + window.SetZoom(zoom); + }); + } + +} diff --git a/src/InfiniFrame.Native/Interop/InitParamsReader.h b/src/InfiniFrame.Native/Interop/InitParamsReader.h new file mode 100644 index 000000000..d65020ed4 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/InitParamsReader.h @@ -0,0 +1,52 @@ +#pragma once +/** + * @file InitParamsReader.h + * @brief Validates InfiniFrameInitParams before platform-specific window setup. + */ + +#ifndef INFINIFRAME_INTEROP_INITPARAMSREADER_H +#define INFINIFRAME_INTEROP_INITPARAMSREADER_H + +#include "../Core/InfiniFrameInitParams.h" + +#include +#include + +namespace InfiniFrame::Native::Interop { + class InitParamsReader { + public: + explicit InitParamsReader(const InfiniFrameInitParams* initParams) : + _params(initParams) { + if (_params == nullptr) + throw std::invalid_argument("InfiniFrameInitParams pointer must not be null."); + + if (_params->Size != sizeof(InfiniFrameInitParams)) { + throw std::invalid_argument( + "Initial parameters passed are " + + std::to_string(_params->Size) + + " bytes, but expected " + + std::to_string(sizeof(InfiniFrameInitParams)) + + " bytes." + ); + } + } + + [[nodiscard]] const InfiniFrameInitParams& Params() const noexcept { + return *_params; + } + + [[nodiscard]] bool HasStartContent() const noexcept { + return _params->StartUrl != nullptr || _params->StartString != nullptr; + } + + void RequireStartContent() const { + if (!HasStartContent()) + throw std::invalid_argument("Either StartUrl or StartString must be specified."); + } + + private: + const InfiniFrameInitParams* _params; + }; +} + +#endif // INFINIFRAME_INTEROP_INITPARAMSREADER_H diff --git a/src/InfiniFrame.Native/Interop/NativeBuffer.h b/src/InfiniFrame.Native/Interop/NativeBuffer.h new file mode 100644 index 000000000..e47ae7067 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/NativeBuffer.h @@ -0,0 +1,43 @@ +#pragma once +/** + * @file NativeBuffer.h + * @brief Ownership helpers for buffers returned across the native interop boundary. + */ + +#ifndef INFINIFRAME_INTEROP_NATIVEBUFFER_H +#define INFINIFRAME_INTEROP_NATIVEBUFFER_H + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include + +namespace InfiniFrame::Native::Interop { + inline void FreeNativeBuffer(void* buffer) noexcept { + if (buffer == nullptr) + return; + +#ifdef _WIN32 + CoTaskMemFree(buffer); +#else + std::free(buffer); +#endif + } + + struct NativeBufferDeleter { + void operator()(void* buffer) const noexcept { + FreeNativeBuffer(buffer); + } + }; + + using NativeBufferPtr = std::unique_ptr; + + inline NativeBufferPtr AdoptNativeBuffer(void* buffer) noexcept { + return NativeBufferPtr(buffer); + } +} + +#endif // INFINIFRAME_INTEROP_NATIVEBUFFER_H diff --git a/src/InfiniFrame.Native/Interop/NativeResult.h b/src/InfiniFrame.Native/Interop/NativeResult.h new file mode 100644 index 000000000..db0f62ca4 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/NativeResult.h @@ -0,0 +1,217 @@ +#pragma once +/** + * @file NativeResult.h + * @brief Helpers for keeping the C ABI boundary exception-safe. + */ + +#ifndef INFINIFRAME_INTEROP_NATIVERESULT_H +#define INFINIFRAME_INTEROP_NATIVERESULT_H + +#include "../Core/InfiniFrameWindow.h" + +#include +#include +#include +#include +#include +#include + +namespace InfiniFrame::Native::Interop { + enum class NativeStatusCode : int32_t { + Success = 0, + InvalidArgument = EINVAL, + OperationFailed = EFAULT + }; + + constexpr int ExportSuccess = static_cast(NativeStatusCode::Success); + constexpr int ExportInvalidArgument = static_cast(NativeStatusCode::InvalidArgument); + constexpr int ExportOperationFailed = static_cast(NativeStatusCode::OperationFailed); + + inline thread_local NativeString LastExportErrorMessage; + + inline NativeString ToNativeErrorMessage(const char* message) { + if (message == nullptr) + return {}; + +#ifdef _WIN32 + const int utf16Length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, message, -1, nullptr, 0); + if (utf16Length > 0) { + std::wstring result(static_cast(utf16Length - 1), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, message, -1, result.data(), utf16Length); + return result; + } + + std::wstring fallback; + fallback.reserve(std::strlen(message)); + for (const unsigned char* current = reinterpret_cast(message); *current != '\0'; ++current) + fallback.push_back(static_cast(*current)); + return fallback; +#else + return message; +#endif + } + + inline void SetExportErrorMessage(const char* message) noexcept { + try { + LastExportErrorMessage = ToNativeErrorMessage(message); + } + catch (...) { + LastExportErrorMessage.clear(); + } + } + + inline void ClearExportErrorMessage() noexcept { + LastExportErrorMessage.clear(); + } + + [[nodiscard]] inline const NativeString& GetExportErrorMessage() noexcept { + return LastExportErrorMessage; + } + + [[nodiscard]] inline NativeStatusCode ToNativeStatusCode(const int error) noexcept { + return static_cast(error); + } + + inline NativeStatusCode SetExportLastError(const int error) noexcept { +#ifdef _WIN32 + SetLastError(static_cast(error)); +#else + errno = error; +#endif + return ToNativeStatusCode(error); + } + + inline NativeStatusCode SetExportSuccess() noexcept { + ClearExportErrorMessage(); + return SetExportLastError(ExportSuccess); + } + + inline NativeStatusCode SetExportInvalidArgument(const char* message = "Invalid native argument.") noexcept { + SetExportErrorMessage(message); + return SetExportLastError(ExportInvalidArgument); + } + + inline NativeStatusCode SetExportOperationFailed(const char* message = "Native operation failed.") noexcept { + SetExportErrorMessage(message); + return SetExportLastError(ExportOperationFailed); + } + + template + void ResetOutput(T* output) noexcept { + if (output != nullptr) + *output = {}; + } + + template + void ResetOutputs(Outputs*... outputs) noexcept { + (ResetOutput(outputs), ...); + } + + template + bool HasOutputs(Outputs*... outputs) noexcept { + return ((outputs != nullptr) && ...); + } + + template + void RunExport(Action&& action) noexcept { + try { + std::forward(action)(); + SetExportSuccess(); + } + catch (const std::invalid_argument& ex) { + SetExportInvalidArgument(ex.what()); + } + catch (const std::exception& ex) { + SetExportOperationFailed(ex.what()); + } + catch (...) { + SetExportOperationFailed("Unknown native exception."); + } + } + + template + NativeStatusCode RunExportStatus(Action&& action) noexcept { + try { + std::forward(action)(); + return SetExportSuccess(); + } + catch (const std::invalid_argument& ex) { + return SetExportInvalidArgument(ex.what()); + } + catch (const std::exception& ex) { + return SetExportOperationFailed(ex.what()); + } + catch (...) { + return SetExportOperationFailed("Unknown native exception."); + } + } + + template + Result RunReturnExport(const Result fallback, Action&& action) noexcept { + try { + Result result = std::forward(action)(); + SetExportSuccess(); + return result; + } + catch (const std::invalid_argument& ex) { + SetExportInvalidArgument(ex.what()); + return fallback; + } + catch (const std::exception& ex) { + SetExportOperationFailed(ex.what()); + return fallback; + } + catch (...) { + SetExportOperationFailed("Unknown native exception."); + return fallback; + } + } + + template + void RunWindowExport(InfiniFrameWindow* instance, Action&& action, Outputs*... outputs) noexcept { + ResetOutputs(outputs...); + if (instance == nullptr || !HasOutputs(outputs...)) { + SetExportInvalidArgument(); + return; + } + + RunExport([&] { + std::forward(action)(*instance); + }); + } + + template + NativeStatusCode RunWindowExportStatus( + InfiniFrameWindow* instance, + Action&& action, + Outputs*... outputs + ) noexcept { + ResetOutputs(outputs...); + if (instance == nullptr || !HasOutputs(outputs...)) + return SetExportInvalidArgument(); + + return RunExportStatus([&] { + std::forward(action)(*instance); + }); + } + + template + Result RunWindowReturnExport( + InfiniFrameWindow* instance, + const Result fallback, + Action&& action, + Outputs*... outputs + ) noexcept { + ResetOutputs(outputs...); + if (instance == nullptr || !HasOutputs(outputs...)) { + SetExportInvalidArgument(); + return fallback; + } + + return RunReturnExport(fallback, [&] { + return std::forward(action)(*instance); + }); + } +} + +#endif // INFINIFRAME_INTEROP_NATIVERESULT_H diff --git a/src/InfiniFrame.Native/Interop/NativeString.h b/src/InfiniFrame.Native/Interop/NativeString.h new file mode 100644 index 000000000..3e7671099 --- /dev/null +++ b/src/InfiniFrame.Native/Interop/NativeString.h @@ -0,0 +1,99 @@ +#pragma once +/** + * @file NativeString.h + * @brief Ownership helpers for strings returned through the native C ABI. + */ + +#ifndef INFINIFRAME_INTEROP_NATIVESTRING_H +#define INFINIFRAME_INTEROP_NATIVESTRING_H + +#include "../Types/Basic.h" + +#include +#include +#include +#include + +#ifdef __linux__ +#include +#endif + +namespace InfiniFrame::Native::Interop { + inline AutoString AllocateNativeStringCopy(AutoStringConst value) { + if (value == nullptr) + value = +#ifdef _WIN32 + L""; +#else + ""; +#endif + +#ifdef _WIN32 + const size_t length = std::wcslen(value); + auto* copy = new wchar_t[length + 1]; + std::memcpy(copy, value, (length + 1) * sizeof(wchar_t)); + return copy; +#elif __linux__ + return g_strdup(value); +#else + const size_t length = std::strlen(value); + auto* copy = static_cast(std::malloc(length + 1)); + if (copy == nullptr) + return nullptr; + + std::memcpy(copy, value, length + 1); + return copy; +#endif + } + + inline AutoString AllocateNativeStringCopy(const NativeString& value) { + return AllocateNativeStringCopy(value.c_str()); + } + + inline AutoString* AllocateNativeStringArray(const int count) { + if (count <= 0) + return nullptr; + +#if defined(_WIN32) || defined(__linux__) + return new AutoString[count](); +#else + return static_cast(std::calloc(static_cast(count), sizeof(AutoString))); +#endif + } + + inline void FreeNativeString(AutoString value) noexcept { + if (value == nullptr) + return; + +#ifdef _WIN32 + delete[] value; +#elif __linux__ + g_free(value); +#else + std::free(value); +#endif + } + + inline void FreeNativeStringArrayContainer(AutoString* values) noexcept { + if (values == nullptr) + return; + +#if defined(_WIN32) || defined(__linux__) + delete[] values; +#else + std::free(values); +#endif + } + + inline void FreeNativeStringArray(AutoString* values, const int count) noexcept { + if (values == nullptr) + return; + + for (int i = 0; i < count; ++i) + FreeNativeString(values[i]); + + FreeNativeStringArrayContainer(values); + } +} + +#endif // INFINIFRAME_INTEROP_NATIVESTRING_H diff --git a/src/InfiniFrame.Native/Platform/Linux/Dialog.cpp b/src/InfiniFrame.Native/Platform/Linux/Dialog.cpp index 43b1c0d9c..1cbfe5e31 100644 --- a/src/InfiniFrame.Native/Platform/Linux/Dialog.cpp +++ b/src/InfiniFrame.Native/Platform/Linux/Dialog.cpp @@ -5,6 +5,7 @@ */ #include "Core/InfiniFrameDialog.h" +#include "Interop/NativeString.h" #include /** @brief Distinguishes which GtkFileChooserAction to configure in ShowDialog */ @@ -122,9 +123,11 @@ AutoString* ShowDialog( if (type == OpenFile || type == OpenFolder) { GSList* pathList = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); int count = g_slist_length(pathList); - char** results = new char*[count]; + char** results = InfiniFrame::Native::Interop::AllocateNativeStringArray(count); for (int i = 0; i < count; i++) { - results[i] = g_strdup(static_cast(g_slist_nth_data(pathList, i))); + results[i] = InfiniFrame::Native::Interop::AllocateNativeStringCopy( + static_cast(g_slist_nth_data(pathList, i)) + ); } g_slist_free_full(pathList, g_free); *resultCount = count; @@ -132,9 +135,14 @@ AutoString* ShowDialog( return results; } else { - char* result = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + char** result = InfiniFrame::Native::Interop::AllocateNativeStringArray(1); + if (result != nullptr) + result[0] = InfiniFrame::Native::Interop::AllocateNativeStringCopy(filename); + + g_free(filename); gtk_widget_destroy(dialog); - return new char*[1]{result}; + return result; } } @@ -174,7 +182,7 @@ AutoString InfiniFrameDialog::ShowSaveFile( char** result = ShowDialog(SaveFile, title, defaultPath, false, filters, filterCount, nullptr, defaultFileName); if (result != nullptr) { char* value = result[0]; - delete[] result; + InfiniFrame::Native::Interop::FreeNativeStringArrayContainer(result); return value; } return nullptr; diff --git a/src/InfiniFrame.Native/Platform/Linux/Monitors.Gtk.cpp b/src/InfiniFrame.Native/Platform/Linux/Monitors.Gtk.cpp new file mode 100644 index 000000000..8e593b096 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/Monitors.Gtk.cpp @@ -0,0 +1,33 @@ +#ifdef __linux__ + +#include "Platform/Linux/WindowImpl.Gtk.h" + +unsigned int InfiniFrameWindow::GetScreenDpi() const { + GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_impl->_window)); + gdouble dpi = gdk_screen_get_resolution(screen); + if (dpi < 0) + return 96; + + return static_cast(dpi); +} + +void InfiniFrameWindow::GetAllMonitors(const GetAllMonitorsCallback callback) const { + if (callback == nullptr) + return; + + GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_impl->_window)); + GdkDisplay* display = gdk_screen_get_display(screen); + const int count = gdk_display_get_n_monitors(display); + for (int i = 0; i < count; i++) { + GdkMonitor* monitor = gdk_display_get_monitor(display, i); + Monitor props = {}; + gdk_monitor_get_geometry(monitor, reinterpret_cast(&props.monitor)); + gdk_monitor_get_workarea(monitor, reinterpret_cast(&props.work)); + props.scale = gdk_monitor_get_scale_factor(monitor); + + if (!callback(&props)) + break; + } +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/Notifications.LibNotify.cpp b/src/InfiniFrame.Native/Platform/Linux/Notifications.LibNotify.cpp new file mode 100644 index 000000000..b3c638787 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/Notifications.LibNotify.cpp @@ -0,0 +1,26 @@ +#ifdef __linux__ + +#include "Platform/Linux/WindowImpl.Gtk.h" + +#include + +void InfiniFrameWindow::Impl::InitializeNotifications(const AutoStringConst appName) const { + notify_init(appName == nullptr ? "InfiniFrame" : appName); +} + +void InfiniFrameWindow::Impl::ShutdownNotifications() const noexcept { + notify_uninit(); +} + +void InfiniFrameWindow::ShowNotification(const AutoString title, const AutoString message) { + NotifyNotification* notification = notify_notification_new( + title == nullptr ? "" : title, + message == nullptr ? "" : message, + nullptr + ); + notify_notification_set_icon_from_pixbuf(notification, gtk_window_get_icon(GTK_WINDOW(m_impl->_window))); + notify_notification_show(notification, nullptr); + g_object_unref(G_OBJECT(notification)); +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/UiDispatcher.Gtk.cpp b/src/InfiniFrame.Native/Platform/Linux/UiDispatcher.Gtk.cpp new file mode 100644 index 000000000..e689ab905 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/UiDispatcher.Gtk.cpp @@ -0,0 +1,47 @@ +#ifdef __linux__ + +#include "Platform/Linux/WindowImpl.Gtk.h" + +#include +#include + +namespace { + std::mutex InvokeLockMutex; + + struct InvokeWaitInfo { + ACTION callback = nullptr; + std::condition_variable completionNotifier; + bool isCompleted = false; + }; + + gboolean InvokeCallback(const gpointer data) { + auto* waitInfo = reinterpret_cast(data); + if (waitInfo->callback != nullptr) + waitInfo->callback(); + { + std::lock_guard guard(InvokeLockMutex); + waitInfo->isCompleted = true; + } + waitInfo->completionNotifier.notify_one(); + return false; + } +} + +void InfiniFrameWindow::Invoke(const ACTION callback) { + if (callback == nullptr) + return; + + InvokeWaitInfo waitInfo = {}; + waitInfo.callback = callback; + gdk_threads_add_idle(InvokeCallback, &waitInfo); + + std::unique_lock uLock(InvokeLockMutex); + waitInfo.completionNotifier.wait( + uLock, + [&] { + return waitInfo.isCompleted; + } + ); +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/WebKitBridge.Gtk.cpp b/src/InfiniFrame.Native/Platform/Linux/WebKitBridge.Gtk.cpp new file mode 100644 index 000000000..c61ff53fd --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/WebKitBridge.Gtk.cpp @@ -0,0 +1,69 @@ +#ifdef __linux__ + +#include "Platform/Linux/WebKitMessaging.Gtk.h" +#include "Platform/Linux/WindowImpl.Gtk.h" + +#include "Embedded/Embedded.h" + +#include +#include + +bool InfiniFrameWindow::Impl::EnsureWebView() { + if (_webview) + return true; + + if (_startUrl.empty() && _startString.empty()) + throw std::invalid_argument("Either StartUrl or StartString must be specified."); + + struct sigaction old_action; + sigaction(SIGCHLD, nullptr, &old_action); + + WebKitUserContentManager* contentManager = webkit_user_content_manager_new(); + _webview = webkit_web_view_new_with_user_content_manager(contentManager); + + set_webkit_settings(); + + gtk_container_add(GTK_CONTAINER(_window), _webview); + + auto js = Embedded::InfiniFrameHostJsUtf8(); + + WebKitUserScript* script = webkit_user_script_new( + js.c_str(), + WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, + WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, + nullptr, + nullptr + ); + + webkit_user_content_manager_add_script(contentManager, script); + webkit_user_script_unref(script); + + _webMessageReceivedHandlerId = g_signal_connect( + contentManager, "script-message-received::infiniFrameInterop", + G_CALLBACK(HandleWebMessage), + reinterpret_cast(_webMessageReceivedCallback) + ); + webkit_user_content_manager_register_script_message_handler(contentManager, "infiniFrameInterop"); + + if (!_startUrl.empty()) + webkit_web_view_load_uri(WEBKIT_WEB_VIEW(_webview), _startUrl.c_str()); + else if (!_startString.empty()) + webkit_web_view_load_html(WEBKIT_WEB_VIEW(_webview), _startString.c_str(), nullptr); + + sigaction(SIGCHLD, &old_action, nullptr); + return true; +} + +void InfiniFrameWindow::NavigateToString(const AutoString content) { + webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_impl->_webview), content, nullptr); +} + +void InfiniFrameWindow::NavigateToUrl(const AutoString url) { + webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_impl->_webview), url); +} + +void InfiniFrameWindow::CloseWebView() { + // Not implemented on Linux +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/WebKitCustomSchemes.Gtk.cpp b/src/InfiniFrame.Native/Platform/Linux/WebKitCustomSchemes.Gtk.cpp new file mode 100644 index 000000000..39f472be4 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/WebKitCustomSchemes.Gtk.cpp @@ -0,0 +1,80 @@ +#ifdef __linux__ + +#include "Platform/Linux/WindowImpl.Gtk.h" +#include "Shared/CustomSchemeResponse.h" + +#include + +namespace { + void FreeNativeBufferForStream(gpointer data) { + InfiniFrame::Native::Interop::FreeNativeBuffer(data); + } + + void FinishCustomSchemeRequestWithError(WebKitURISchemeRequest* request, const char* message) { + GError* error = g_error_new_literal( + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + message); + webkit_uri_scheme_request_finish_error(request, error); + g_error_free(error); + } +} + +static void HandleCustomSchemeRequest(WebKitURISchemeRequest* request, const gpointer user_data) { + WebResourceRequestedCallback webResourceRequestedCallback = reinterpret_cast( + user_data); + if (webResourceRequestedCallback == nullptr) { + FinishCustomSchemeRequestWithError(request, "No custom scheme handler is registered."); + return; + } + + const gchar* uri = webkit_uri_scheme_request_get_uri(request); + auto dotNetResponse = InfiniFrame::Native::Shared::InvokeCustomSchemeCallback( + webResourceRequestedCallback, + const_cast(uri) + ); + + if (!dotNetResponse.HasBody()) { + FinishCustomSchemeRequestWithError(request, "Custom scheme handler returned no response."); + return; + } + + GInputStream* stream = g_memory_input_stream_new_from_data( + dotNetResponse.body.get(), + dotNetResponse.length, + FreeNativeBufferForStream + ); + dotNetResponse.body.release(); + + webkit_uri_scheme_request_finish( + request, + reinterpret_cast(stream), + -1, + dotNetResponse.ContentTypeOrDefault() + ); + g_object_unref(stream); +} + +void InfiniFrameWindow::Impl::AddCustomSchemeHandlers() { + if (_customSchemeCallback == nullptr) + return; + + WebKitWebContext* context = webkit_web_context_get_default(); + WebKitSecurityManager* securityManager = webkit_web_context_get_security_manager(context); + for (const auto& value : _customSchemeNames) { + if (securityManager != nullptr && g_ascii_strcasecmp(value.c_str(), "app") == 0) { + // Mirror Windows behavior for embedded static assets: + // only app:// is explicitly treated as a secure custom scheme. + webkit_security_manager_register_uri_scheme_as_secure(securityManager, value.c_str()); + } + + webkit_web_context_register_uri_scheme( + context, value.c_str(), + reinterpret_cast(HandleCustomSchemeRequest), + reinterpret_cast(_customSchemeCallback), + nullptr + ); + } +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/WebKitMessaging.Gtk.cpp b/src/InfiniFrame.Native/Platform/Linux/WebKitMessaging.Gtk.cpp new file mode 100644 index 000000000..067204b69 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/WebKitMessaging.Gtk.cpp @@ -0,0 +1,122 @@ +#ifdef __linux__ + +#include "Platform/Linux/WebKitMessaging.Gtk.h" +#include "Platform/Linux/WindowImpl.Gtk.h" +#include "Utils/Common.h" + +#include +#include +#include +#include +#include + +static std::string escapeJsonString(std::string_view input) { + std::string result; + result.reserve(input.size() + 2); + + for (char c : input) { + switch (c) { + case '"': + result += "\\\""; + break; + case '\\': + result += "\\\\"; + break; + case '\b': + result += "\\b"; + break; + case '\f': + result += "\\f"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + default: + if (static_cast(c) < 0x20) { + std::format_to(std::back_inserter(result), "\\u{:04x}", static_cast(c)); + } + else { + result += c; + } + } + } + + return result; +} + +void HandleWebMessage( + WebKitUserContentManager* contentManager, + WebKitJavascriptResult* jsResult, + const gpointer userData + ) { + (void)contentManager; + + JSCValue* jsValue = webkit_javascript_result_get_js_value(jsResult); + if (jsc_value_is_string(jsValue)) { + AutoString str_value = jsc_value_to_string(jsValue); + WebMessageReceivedCallback callback = reinterpret_cast(userData); + AutoString originValue = nullptr; + + JSGlobalContextRef context = webkit_javascript_result_get_global_context(jsResult); + JSStringRef script = JSStringCreateWithUTF8CString("window.location.href"); + JSValueRef locationValue = JSEvaluateScript(context, script, nullptr, nullptr, 0, nullptr); + JSStringRelease(script); + + if (locationValue != nullptr) { + JSStringRef locationString = JSValueToStringCopy(context, locationValue, nullptr); + if (locationString != nullptr) { + size_t maxBytes = JSStringGetMaximumUTF8CStringSize(locationString); + originValue = static_cast(g_malloc(maxBytes)); + JSStringGetUTF8CString(locationString, originValue, maxBytes); + JSStringRelease(locationString); + } + } + + if (callback != nullptr) { + callback(str_value, originValue); + } + + if (originValue != nullptr) + g_free(originValue); + + g_free(str_value); + } + webkit_javascript_result_unref(jsResult); +} + +static void webview_eval_finished(GObject* object, GAsyncResult* result, gpointer) { + GError* error = nullptr; + webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(object), result, &error); + if (error) { + g_warning("JavaScript evaluation failed: %s", error->message); + g_error_free(error); + } +} + +void InfiniFrameWindow::SendWebMessage(const AutoString message) { + std::string escaped = escapeJsonString(message ? message : ""); + + std::string js; + js.append("__dispatchMessageCallback(\""); + js.append(escaped); + js.append("\")"); + + webkit_web_view_evaluate_javascript( + WEBKIT_WEB_VIEW(m_impl->_webview), + js.c_str(), + -1, + nullptr, + nullptr, + nullptr, + webview_eval_finished, + nullptr + ); +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/WebKitMessaging.Gtk.h b/src/InfiniFrame.Native/Platform/Linux/WebKitMessaging.Gtk.h new file mode 100644 index 000000000..2e581908f --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/WebKitMessaging.Gtk.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef __linux__ + +#include + +void HandleWebMessage( + WebKitUserContentManager* contentManager, + WebKitJavascriptResult* jsResult, + gpointer userData + ); + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/WebKitSettings.Gtk.cpp b/src/InfiniFrame.Native/Platform/Linux/WebKitSettings.Gtk.cpp new file mode 100644 index 000000000..596f55ce6 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/WebKitSettings.Gtk.cpp @@ -0,0 +1,115 @@ +#ifdef __linux__ + +#include "Platform/Linux/WindowImpl.Gtk.h" + +#include +#include +#include +#include + +void InfiniFrameWindow::Impl::set_webkit_settings() { + WebKitSettings* settings = webkit_settings_new_with_settings( + "allow_modal_dialogs", TRUE, + "allow_top_navigation_to_data_urls", TRUE, + "allow_universal_access_from_file_urls", TRUE, + "enable_back_forward_navigation_gestures", TRUE, + "enable_media_capabilities", TRUE, + "enable_mock_capture_devices", TRUE, + "enable_page_cache", TRUE, + "enable_webrtc", TRUE, + "javascript_can_open_windows_automatically", TRUE, + + "allow_file_access_from_file_urls", _fileSystemAccessEnabled, + "disable_web_security", !_webSecurityEnabled, + "enable_developer_extras", _devToolsEnabled, + "enable_media_stream", _mediaStreamEnabled, + "enable_smooth_scrolling", _smoothScrollingEnabled, + "javascript_can_access_clipboard", _javascriptClipboardAccessEnabled, + "media_playback_requires_user_gesture", !_mediaAutoplayEnabled, + "user_agent", _userAgent.c_str(), + + NULL + ); + + if (!_browserControlInitParameters.empty()) + set_webkit_customsettings(settings); + + WebKitWebsiteDataManager* manager = webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(_webview)); + if (_ignoreCertificateErrorsEnabled) + webkit_website_data_manager_set_tls_errors_policy(manager, WEBKIT_TLS_ERRORS_POLICY_IGNORE); + else + webkit_website_data_manager_set_tls_errors_policy(manager, WEBKIT_TLS_ERRORS_POLICY_FAIL); + + webkit_web_view_set_settings(WEBKIT_WEB_VIEW(_webview), settings); +} + +void InfiniFrameWindow::Impl::set_webkit_customsettings(WebKitSettings* settings) { + try { + simdjson::ondemand::parser parser; + auto padded = simdjson::padded_string(_browserControlInitParameters); + auto doc = parser.iterate(padded); + + for (auto field : doc.get_object()) { + std::string_view keyView = field.unescaped_key(); + auto value = field.value(); + + gchar* propertyName = g_strdup(std::string(keyView).c_str()); + GValue propertyValue = G_VALUE_INIT; + bool hasValidValue = false; + + switch (value.type()) { + case simdjson::ondemand::json_type::string: { + std::string_view strVal; + if (value.get(strVal) == simdjson::SUCCESS) { + g_value_init(&propertyValue, G_TYPE_STRING); + g_value_set_string(&propertyValue, std::string(strVal).c_str()); + hasValidValue = true; + } + break; + } + case simdjson::ondemand::json_type::boolean: { + bool boolVal; + if (value.get(boolVal) == simdjson::SUCCESS) { + g_value_init(&propertyValue, G_TYPE_BOOLEAN); + g_value_set_boolean(&propertyValue, boolVal); + hasValidValue = true; + } + break; + } + case simdjson::ondemand::json_type::number: { + int64_t intVal; + if (value.get(intVal) == simdjson::SUCCESS) { + g_value_init(&propertyValue, G_TYPE_INT); + g_value_set_int(&propertyValue, static_cast(intVal)); + hasValidValue = true; + } + else { + double doubleVal; + if (value.get(doubleVal) == simdjson::SUCCESS) { + g_value_init(&propertyValue, G_TYPE_DOUBLE); + g_value_set_double(&propertyValue, doubleVal); + hasValidValue = true; + } + } + break; + } + default: + // Ignore unsupported JSON value types instead of crashing. + break; + } + + if (hasValidValue) { + g_object_set_property(G_OBJECT(settings), propertyName, &propertyValue); + g_value_unset(&propertyValue); + } + + g_free(propertyName); + } + } + catch (const simdjson::simdjson_error&) { + // Some callers pass CLI-like strings (e.g. --remote-debugging-port=9222). + // Ignore non-JSON payloads instead of aborting the process. + } +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/Window.cpp b/src/InfiniFrame.Native/Platform/Linux/Window.cpp index 7de503c1b..db42d0bd2 100644 --- a/src/InfiniFrame.Native/Platform/Linux/Window.cpp +++ b/src/InfiniFrame.Native/Platform/Linux/Window.cpp @@ -1,32 +1,17 @@ #ifdef __linux__ -#include "Core/InfiniFrameWindow.h" #include "Core/InfiniFrameDialog.h" -#include "Core/InfiniFrameWindowImpl.h" +#include "Interop/InitParamsReader.h" +#include "Platform/Linux/WindowImpl.Gtk.h" #include "Utils/Common.h" -#include -#include -#include + +#include +#include #include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include "Embedded/Embedded.h" - -std::mutex invokeLockMutex; - -struct InvokeWaitInfo { - ACTION callback; - std::condition_variable completionNotifier; - bool isCompleted; -}; // Forward declarations for GTK signal handlers +static void disconnect_signal(GObject* instance, gulong& handlerId) noexcept; + gboolean on_configure_event(GtkWidget* widget, GdkEvent* event, gpointer self); gboolean on_window_state_event(GtkWidget* widget, GdkEventWindowState* event, gpointer self); gboolean on_widget_deleted(GtkWidget* widget, GdkEvent* event, gpointer self); @@ -41,302 +26,18 @@ gboolean on_webview_context_menu( ); gboolean on_permission_request(WebKitWebView* web_view, WebKitPermissionRequest* request, gpointer user_data); -// --------------------------------------------------------------------------------------------------------------------- -// Platform Impl -// --------------------------------------------------------------------------------------------------------------------- - -struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { - GtkWidget* _window = nullptr; - GtkWidget* _webview = nullptr; - - std::string _temporaryFilesPath; - - bool _isFullScreen = false; - double _zoom = 100.0; - int _minWidth = 0; - int _minHeight = 0; - int _maxWidth = INT_MAX; - int _maxHeight = INT_MAX; - - GdkGeometry _hints = {}; - - int _lastLeft = 0; - int _lastTop = 0; - int _lastWidth = 0; - int _lastHeight = 0; - - void set_webkit_settings(); - void set_webkit_customsettings(WebKitSettings* settings); - void AddCustomSchemeHandlers(); -}; - -// --------------------------------------------------------------------------------------------------------------------- -// Static signal handlers and helpers -// --------------------------------------------------------------------------------------------------------------------- - -static gboolean invokeCallback(const gpointer data) { - auto* waitInfo = reinterpret_cast(data); - waitInfo->callback(); - { - std::lock_guard guard(invokeLockMutex); - waitInfo->isCompleted = true; - } - waitInfo->completionNotifier.notify_one(); - return false; -} - -static void HandleWebMessage( - WebKitUserContentManager* contentManager, - WebKitJavascriptResult* jsResult, - const gpointer userData - ) { - JSCValue* jsValue = webkit_javascript_result_get_js_value(jsResult); - if (jsc_value_is_string(jsValue)) { - AutoString str_value = jsc_value_to_string(jsValue); - WebMessageReceivedCallback callback = reinterpret_cast(userData); - AutoString originValue = nullptr; - - JSGlobalContextRef context = webkit_javascript_result_get_global_context(jsResult); - JSStringRef script = JSStringCreateWithUTF8CString("window.location.href"); - JSValueRef locationValue = JSEvaluateScript(context, script, nullptr, nullptr, 0, nullptr); - JSStringRelease(script); - - if (locationValue != nullptr) { - JSStringRef locationString = JSValueToStringCopy(context, locationValue, nullptr); - if (locationString != nullptr) { - size_t maxBytes = JSStringGetMaximumUTF8CStringSize(locationString); - originValue = static_cast(g_malloc(maxBytes)); - JSStringGetUTF8CString(locationString, originValue, maxBytes); - JSStringRelease(locationString); - } - } - - if (callback != nullptr) { - callback(str_value, originValue); - } - - if (originValue != nullptr) - g_free(originValue); - - g_free(str_value); - } - webkit_javascript_result_unref(jsResult); -} - -static void HandleCustomSchemeRequest(WebKitURISchemeRequest* request, const gpointer user_data) { - WebResourceRequestedCallback webResourceRequestedCallback = reinterpret_cast( - user_data); - if (webResourceRequestedCallback == nullptr) { - GError* error = g_error_new_literal( - G_IO_ERROR, - G_IO_ERROR_NOT_SUPPORTED, - "No custom scheme handler is registered."); - webkit_uri_scheme_request_finish_error(request, error); - g_error_free(error); - return; - } - - const gchar* uri = webkit_uri_scheme_request_get_uri(request); - int numBytes = 0; - AutoString contentType = nullptr; - void* dotNetResponse = webResourceRequestedCallback(const_cast(uri), &numBytes, &contentType); - GInputStream* stream = g_memory_input_stream_new_from_data(dotNetResponse, numBytes, nullptr); - webkit_uri_scheme_request_finish(request, reinterpret_cast(stream), -1, contentType); - g_object_unref(stream); - free(contentType); -} - -static std::string escapeJsonString(std::string_view input) { - std::string result; - result.reserve(input.size() + 2); - - for (char c : input) { - switch (c) { - case '"': - result += "\\\""; - break; - case '\\': - result += "\\\\"; - break; - case '\b': - result += "\\b"; - break; - case '\f': - result += "\\f"; - break; - case '\n': - result += "\\n"; - break; - case '\r': - result += "\\r"; - break; - case '\t': - result += "\\t"; - break; - default: - if (static_cast(c) < 0x20) { - std::format_to(std::back_inserter(result), "\\u{:04x}", static_cast(c)); - } - else { - result += c; - } - } - } - - return result; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Impl method definitions -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::Impl::set_webkit_settings() { - WebKitSettings* settings = webkit_settings_new_with_settings( - "allow_modal_dialogs", TRUE, - "allow_top_navigation_to_data_urls", TRUE, - "allow_universal_access_from_file_urls", TRUE, - "enable_back_forward_navigation_gestures", TRUE, - "enable_media_capabilities", TRUE, - "enable_mock_capture_devices", TRUE, - "enable_page_cache", TRUE, - "enable_webrtc", TRUE, - "javascript_can_open_windows_automatically", TRUE, - - "allow_file_access_from_file_urls", _fileSystemAccessEnabled, - "disable_web_security", !_webSecurityEnabled, - "enable_developer_extras", _devToolsEnabled, - "enable_media_stream", _mediaStreamEnabled, - "enable_smooth_scrolling", _smoothScrollingEnabled, - "javascript_can_access_clipboard", _javascriptClipboardAccessEnabled, - "media_playback_requires_user_gesture", !_mediaAutoplayEnabled, - "user_agent", _userAgent.c_str(), - - NULL - ); - - if (!_browserControlInitParameters.empty()) - set_webkit_customsettings(settings); - - WebKitWebsiteDataManager* manager = webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(_webview)); - if (_ignoreCertificateErrorsEnabled) - webkit_website_data_manager_set_tls_errors_policy(manager, WEBKIT_TLS_ERRORS_POLICY_IGNORE); - else - webkit_website_data_manager_set_tls_errors_policy(manager, WEBKIT_TLS_ERRORS_POLICY_FAIL); - - webkit_web_view_set_settings(WEBKIT_WEB_VIEW(_webview), settings); -} - -void InfiniFrameWindow::Impl::set_webkit_customsettings(WebKitSettings* settings) { - try { - simdjson::ondemand::parser parser; - auto padded = simdjson::padded_string(_browserControlInitParameters); - auto doc = parser.iterate(padded); - - for (auto field : doc.get_object()) { - std::string_view keyView = field.unescaped_key(); - auto value = field.value(); - - gchar* propertyName = g_strdup(std::string(keyView).c_str()); - GValue propertyValue = G_VALUE_INIT; - bool hasValidValue = false; - - switch (value.type()) { - case simdjson::ondemand::json_type::string: { - std::string_view strVal; - if (value.get(strVal) == simdjson::SUCCESS) { - g_value_init(&propertyValue, G_TYPE_STRING); - g_value_set_string(&propertyValue, std::string(strVal).c_str()); - hasValidValue = true; - } - break; - } - case simdjson::ondemand::json_type::boolean: { - bool boolVal; - if (value.get(boolVal) == simdjson::SUCCESS) { - g_value_init(&propertyValue, G_TYPE_BOOLEAN); - g_value_set_boolean(&propertyValue, boolVal); - hasValidValue = true; - } - break; - } - case simdjson::ondemand::json_type::number: { - int64_t intVal; - if (value.get(intVal) == simdjson::SUCCESS) { - g_value_init(&propertyValue, G_TYPE_INT); - g_value_set_int(&propertyValue, static_cast(intVal)); - hasValidValue = true; - } - else { - double doubleVal; - if (value.get(doubleVal) == simdjson::SUCCESS) { - g_value_init(&propertyValue, G_TYPE_DOUBLE); - g_value_set_double(&propertyValue, doubleVal); - hasValidValue = true; - } - } - break; - } - default: - // Ignore unsupported JSON value types instead of crashing. - break; - } - - if (hasValidValue) { - g_object_set_property(G_OBJECT(settings), propertyName, &propertyValue); - g_value_unset(&propertyValue); - } - - g_free(propertyName); - } - } - catch (const simdjson::simdjson_error&) { - // Some callers pass CLI-like strings (e.g. --remote-debugging-port=9222). - // Ignore non-JSON payloads instead of aborting the process. - } -} - -void InfiniFrameWindow::Impl::AddCustomSchemeHandlers() { - if (_customSchemeCallback == nullptr) - return; - - WebKitWebContext* context = webkit_web_context_get_default(); - WebKitSecurityManager* securityManager = webkit_web_context_get_security_manager(context); - for (const auto& value : _customSchemeNames) { - if (securityManager != nullptr && g_ascii_strcasecmp(value.c_str(), "app") == 0) { - // Mirror Windows behavior for embedded static assets: - // only app:// is explicitly treated as a secure custom scheme. - webkit_security_manager_register_uri_scheme_as_secure(securityManager, value.c_str()); - } - - webkit_web_context_register_uri_scheme( - context, value.c_str(), - reinterpret_cast(HandleCustomSchemeRequest), - reinterpret_cast(_customSchemeCallback), - nullptr - ); - } -} - // --------------------------------------------------------------------------------------------------------------------- // Constructor / Destructor // --------------------------------------------------------------------------------------------------------------------- InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : m_impl(std::make_unique()) { + const auto initParamsReader = InfiniFrame::Native::Interop::InitParamsReader(initParams); + initParamsReader.RequireStartContent(); + XInitThreads(); gtk_init(nullptr, nullptr); - notify_init(initParams->Title); - - if (initParams->Size != sizeof(InfiniFrameInitParams)) { - GtkWidget* dialog = gtk_message_dialog_new( - nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, - "Initial parameters passed are %i bytes, but expected %lu bytes.", - initParams->Size, sizeof(InfiniFrameInitParams) - ); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - exit(0); - } + m_impl->InitializeNotifications(initParams->Title); m_impl->_windowTitle = initParams->Title ? initParams->Title : ""; @@ -346,6 +47,9 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : if (initParams->StartString != nullptr) m_impl->_startString = initParams->StartString; + if (m_impl->_startUrl.empty() && m_impl->_startString.empty()) + throw std::invalid_argument("Either StartUrl or StartString must be specified."); + if (initParams->TemporaryFilesPath != nullptr) m_impl->_temporaryFilesPath = initParams->TemporaryFilesPath; @@ -447,39 +151,39 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : if (initParams->Topmost) SetTopmost(true); - g_signal_connect( + m_impl->_configureEventHandlerId = g_signal_connect( G_OBJECT(m_impl->_window), "configure-event", G_CALLBACK(on_configure_event), this ); - g_signal_connect( + m_impl->_windowStateEventHandlerId = g_signal_connect( G_OBJECT(m_impl->_window), "window-state-event", G_CALLBACK(on_window_state_event), this ); - g_signal_connect( + m_impl->_deleteEventHandlerId = g_signal_connect( G_OBJECT(m_impl->_window), "delete-event", G_CALLBACK(on_widget_deleted), this ); Show(false); - g_signal_connect( + m_impl->_focusInEventHandlerId = g_signal_connect( G_OBJECT(m_impl->_window), "focus-in-event", G_CALLBACK(on_focus_in_event), this ); - g_signal_connect( + m_impl->_focusOutEventHandlerId = g_signal_connect( G_OBJECT(m_impl->_window), "focus-out-event", G_CALLBACK(on_focus_out_event), this ); - g_signal_connect( + m_impl->_contextMenuHandlerId = g_signal_connect( G_OBJECT(m_impl->_webview), "context-menu", G_CALLBACK(on_webview_context_menu), this ); - g_signal_connect( + m_impl->_permissionRequestHandlerId = g_signal_connect( G_OBJECT(m_impl->_webview), "permission-request", G_CALLBACK(on_permission_request), this ); @@ -494,7 +198,8 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : } InfiniFrameWindow::~InfiniFrameWindow() { - notify_uninit(); + m_impl->DisconnectSignalHandlers(); + m_impl->ShutdownNotifications(); gtk_widget_destroy(m_impl->_window); } @@ -571,10 +276,6 @@ void InfiniFrameWindow::GetDevToolsEnabled(bool* enabled) const { *enabled = webkit_settings_get_enable_developer_extras(settings); } -void InfiniFrameWindow::GetFullScreen(bool* fullScreen) const { - *fullScreen = m_impl->_isFullScreen; -} - void InfiniFrameWindow::GetGrantBrowserPermissions(bool* grant) const { *grant = m_impl->_grantBrowserPermissions; } @@ -631,15 +332,6 @@ void InfiniFrameWindow::GetResizable(bool* resizable) const { *resizable = gtk_window_get_resizable(GTK_WINDOW(m_impl->_window)); } -unsigned int InfiniFrameWindow::GetScreenDpi() const { - GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_impl->_window)); - gdouble dpi = gdk_screen_get_resolution(screen); - if (dpi < 0) - return 96; - else - return static_cast(dpi); -} - void InfiniFrameWindow::GetSize(int* width, int* height) const { gtk_window_get_size(GTK_WINDOW(m_impl->_window), width, height); } @@ -660,7 +352,7 @@ void InfiniFrameWindow::GetMinSize(int* width, int* height) const { AutoString InfiniFrameWindow::GetTitle() const { const char* title = gtk_window_get_title(GTK_WINDOW(m_impl->_window)); - return g_strdup(title ? title : ""); + return InfiniFrame::Native::Interop::AllocateNativeStringCopy(title); } void InfiniFrameWindow::GetTopmost(bool* topmost) const { @@ -683,51 +375,6 @@ AutoString InfiniFrameWindow::GetIconFileName() const { return AllocateStringCopy(m_impl->_iconFileName); } -// --------------------------------------------------------------------------------------------------------------------- -// Navigation -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::NavigateToString(const AutoString content) { - webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_impl->_webview), content, nullptr); -} - -void InfiniFrameWindow::NavigateToUrl(const AutoString url) { - webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_impl->_webview), url); -} - -void InfiniFrameWindow::Restore() { - gtk_window_present(GTK_WINDOW(m_impl->_window)); -} - -static void webview_eval_finished(GObject* object, GAsyncResult* result, gpointer) { - GError* error = nullptr; - webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(object), result, &error); - if (error) { - g_warning("JavaScript evaluation failed: %s", error->message); - g_error_free(error); - } -} - -void InfiniFrameWindow::SendWebMessage(const AutoString message) { - std::string escaped = escapeJsonString(message ? message : ""); - - std::string js; - js.append("__dispatchMessageCallback(\""); - js.append(escaped); - js.append("\")"); - - webkit_web_view_evaluate_javascript( - WEBKIT_WEB_VIEW(m_impl->_webview), - js.c_str(), - -1, - nullptr, - nullptr, - nullptr, - webview_eval_finished, - nullptr - ); -} - // --------------------------------------------------------------------------------------------------------------------- // Set Properties // --------------------------------------------------------------------------------------------------------------------- @@ -746,34 +393,11 @@ void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { webkit_settings_set_enable_developer_extras(settings, m_impl->_devToolsEnabled); } -void InfiniFrameWindow::SetFullScreen(const bool fullScreen) { - if (fullScreen) - gtk_window_fullscreen(GTK_WINDOW(m_impl->_window)); - else - gtk_window_unfullscreen(GTK_WINDOW(m_impl->_window)); - - m_impl->_isFullScreen = fullScreen; -} - void InfiniFrameWindow::SetIconFile(const AutoString filename) { gtk_window_set_icon_from_file(GTK_WINDOW(m_impl->_window), filename, nullptr); m_impl->_iconFileName = filename ? filename : ""; } -void InfiniFrameWindow::SetMinimized(const bool minimized) { - if (minimized) - gtk_window_iconify(GTK_WINDOW(m_impl->_window)); - else - gtk_window_deiconify(GTK_WINDOW(m_impl->_window)); -} - -void InfiniFrameWindow::SetMaximized(const bool maximized) { - if (maximized) - gtk_window_maximize(GTK_WINDOW(m_impl->_window)); - else - gtk_window_unmaximize(GTK_WINDOW(m_impl->_window)); -} - void InfiniFrameWindow::SetPosition(const int x, const int y) { gtk_window_move(GTK_WINDOW(m_impl->_window), x, y); } @@ -849,19 +473,8 @@ void InfiniFrameWindow::SetTransparentEnabled(const bool enabled) { } } -// --------------------------------------------------------------------------------------------------------------------- -// Notifications / Event loop -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::ShowNotification(const AutoString title, const AutoString message) { - NotifyNotification* notification = notify_notification_new(title, message, nullptr); - notify_notification_set_icon_from_pixbuf(notification, gtk_window_get_icon(GTK_WINDOW(m_impl->_window))); - notify_notification_show(notification, nullptr); - g_object_unref(G_OBJECT(notification)); -} - void InfiniFrameWindow::WaitForExit() { - g_signal_connect( + m_impl->_destroyHandlerId = g_signal_connect( G_OBJECT(m_impl->_window), "destroy", G_CALLBACK( +[](GtkWidget*, gpointer) { @@ -873,10 +486,6 @@ void InfiniFrameWindow::WaitForExit() { gtk_main(); } -void InfiniFrameWindow::CloseWebView() { - // Not implemented on Linux -} - // --------------------------------------------------------------------------------------------------------------------- // Callbacks // --------------------------------------------------------------------------------------------------------------------- @@ -890,23 +499,6 @@ void InfiniFrameWindow::AddCustomSchemeName(const AutoStringConst scheme) { m_impl->_customSchemeNames.emplace_back(scheme); } -void InfiniFrameWindow::GetAllMonitors(const GetAllMonitorsCallback callback) const { - if (callback) { - GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_impl->_window)); - GdkDisplay* display = gdk_screen_get_display(screen); - int n = gdk_display_get_n_monitors(display); - for (int i = 0; i < n; i++) { - GdkMonitor* monitor = gdk_display_get_monitor(display, i); - Monitor props = {}; - gdk_monitor_get_geometry(monitor, (GdkRectangle*)&props.monitor); - gdk_monitor_get_workarea(monitor, (GdkRectangle*)&props.work); - props.scale = gdk_monitor_get_scale_factor(monitor); - if (!callback(&props)) - break; - } - } -} - void InfiniFrameWindow::SetClosingCallback(const ClosingCallback callback) { m_impl->_closingCallback = callback; } @@ -939,19 +531,6 @@ void InfiniFrameWindow::SetMinimizedCallback(const MinimizedCallback callback) { m_impl->_minimizedCallback = callback; } -void InfiniFrameWindow::Invoke(const ACTION callback) { - InvokeWaitInfo waitInfo = {}; - waitInfo.callback = callback; - gdk_threads_add_idle(invokeCallback, &waitInfo); - - std::unique_lock uLock(invokeLockMutex); - waitInfo.completionNotifier.wait( - uLock, [&] { - return waitInfo.isCompleted; - } - ); -} - [[nodiscard]] bool InfiniFrameWindow::InvokeClose() const noexcept { if (m_impl->_closingCallback) return m_impl->_closingCallback(); @@ -998,52 +577,10 @@ void InfiniFrameWindow::InvokeMinimized() const noexcept { // --------------------------------------------------------------------------------------------------------------------- void InfiniFrameWindow::Show(bool isAlreadyShown) { - if (!m_impl->_webview) { - struct sigaction old_action; - sigaction(SIGCHLD, nullptr, &old_action); - WebKitUserContentManager* contentManager = webkit_user_content_manager_new(); - m_impl->_webview = webkit_web_view_new_with_user_content_manager(contentManager); + (void)isAlreadyShown; - m_impl->set_webkit_settings(); - - gtk_container_add(GTK_CONTAINER(m_impl->_window), m_impl->_webview); - - auto js = Embedded::InfiniFrameHostJsUtf8(); - - WebKitUserScript* script = webkit_user_script_new( - js.c_str(), - WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, - WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, - nullptr, - nullptr - ); - - webkit_user_content_manager_add_script(contentManager, script); - webkit_user_script_unref(script); - - g_signal_connect( - contentManager, "script-message-received::infiniFrameInterop", - G_CALLBACK(HandleWebMessage), - reinterpret_cast(m_impl->_webMessageReceivedCallback) - ); - webkit_user_content_manager_register_script_message_handler(contentManager, "infiniFrameInterop"); - - if (!m_impl->_startUrl.empty()) - NavigateToUrl(const_cast(m_impl->_startUrl.c_str())); - else if (!m_impl->_startString.empty()) - NavigateToString(const_cast(m_impl->_startString.c_str())); - else { - GtkWidget* dialog = gtk_message_dialog_new( - nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, - "Neither StartUrl nor StartString was specified" - ); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - sigaction(SIGCHLD, &old_action, nullptr); - return; - } - sigaction(SIGCHLD, &old_action, nullptr); - } + if (!m_impl->EnsureWebView()) + return; gtk_widget_show_all(m_impl->_window); } @@ -1082,6 +619,36 @@ void InfiniFrameWindow::OnWindowStateEvent(GdkWindowState newState) { // GTK Signal Handlers // --------------------------------------------------------------------------------------------------------------------- +static void disconnect_signal(GObject* instance, gulong& handlerId) noexcept { + if (instance == nullptr || handlerId == 0) + return; + + g_signal_handler_disconnect(instance, handlerId); + handlerId = 0; +} + +void InfiniFrameWindow::Impl::DisconnectSignalHandlers() noexcept { + disconnect_signal(G_OBJECT(_window), _configureEventHandlerId); + disconnect_signal(G_OBJECT(_window), _windowStateEventHandlerId); + disconnect_signal(G_OBJECT(_window), _deleteEventHandlerId); + disconnect_signal(G_OBJECT(_window), _focusInEventHandlerId); + disconnect_signal(G_OBJECT(_window), _focusOutEventHandlerId); + disconnect_signal(G_OBJECT(_window), _destroyHandlerId); + + if (_webview == nullptr) + return; + + disconnect_signal(G_OBJECT(_webview), _contextMenuHandlerId); + disconnect_signal(G_OBJECT(_webview), _permissionRequestHandlerId); + + WebKitUserContentManager* contentManager = webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(_webview)); + if (contentManager == nullptr) + return; + + disconnect_signal(G_OBJECT(contentManager), _webMessageReceivedHandlerId); + webkit_user_content_manager_unregister_script_message_handler(contentManager, "infiniFrameInterop"); +} + gboolean on_configure_event(GtkWidget* widget, GdkEvent* event, const gpointer self) { if (event->type == GDK_CONFIGURE) { auto* instance = reinterpret_cast(self); diff --git a/src/InfiniFrame.Native/Platform/Linux/WindowImpl.Gtk.h b/src/InfiniFrame.Native/Platform/Linux/WindowImpl.Gtk.h new file mode 100644 index 000000000..99c902872 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/WindowImpl.Gtk.h @@ -0,0 +1,52 @@ +#pragma once + +#ifdef __linux__ + +#include "Core/InfiniFrameWindow.h" +#include "Core/InfiniFrameWindowImpl.h" + +#include +#include +#include +#include + +struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { + GtkWidget* _window = nullptr; + GtkWidget* _webview = nullptr; + + std::string _temporaryFilesPath; + + bool _isFullScreen = false; + double _zoom = 100.0; + int _minWidth = 0; + int _minHeight = 0; + int _maxWidth = INT_MAX; + int _maxHeight = INT_MAX; + + GdkGeometry _hints = {}; + + gulong _configureEventHandlerId = 0; + gulong _windowStateEventHandlerId = 0; + gulong _deleteEventHandlerId = 0; + gulong _focusInEventHandlerId = 0; + gulong _focusOutEventHandlerId = 0; + gulong _contextMenuHandlerId = 0; + gulong _permissionRequestHandlerId = 0; + gulong _destroyHandlerId = 0; + gulong _webMessageReceivedHandlerId = 0; + + int _lastLeft = 0; + int _lastTop = 0; + int _lastWidth = 0; + int _lastHeight = 0; + + void set_webkit_settings(); + void set_webkit_customsettings(WebKitSettings* settings); + void AddCustomSchemeHandlers(); + [[nodiscard]] bool EnsureWebView(); + void DisconnectSignalHandlers() noexcept; + void InitializeNotifications(AutoStringConst appName) const; + void ShutdownNotifications() const noexcept; +}; + +#endif diff --git a/src/InfiniFrame.Native/Platform/Linux/WindowState.Gtk.cpp b/src/InfiniFrame.Native/Platform/Linux/WindowState.Gtk.cpp new file mode 100644 index 000000000..195a44e75 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Linux/WindowState.Gtk.cpp @@ -0,0 +1,36 @@ +#ifdef __linux__ + +#include "Platform/Linux/WindowImpl.Gtk.h" + +void InfiniFrameWindow::GetFullScreen(bool* fullScreen) const { + *fullScreen = m_impl->_isFullScreen; +} + +void InfiniFrameWindow::Restore() { + gtk_window_present(GTK_WINDOW(m_impl->_window)); +} + +void InfiniFrameWindow::SetFullScreen(const bool fullScreen) { + if (fullScreen) + gtk_window_fullscreen(GTK_WINDOW(m_impl->_window)); + else + gtk_window_unfullscreen(GTK_WINDOW(m_impl->_window)); + + m_impl->_isFullScreen = fullScreen; +} + +void InfiniFrameWindow::SetMinimized(const bool minimized) { + if (minimized) + gtk_window_iconify(GTK_WINDOW(m_impl->_window)); + else + gtk_window_deiconify(GTK_WINDOW(m_impl->_window)); +} + +void InfiniFrameWindow::SetMaximized(const bool maximized) { + if (maximized) + gtk_window_maximize(GTK_WINDOW(m_impl->_window)); + else + gtk_window_unmaximize(GTK_WINDOW(m_impl->_window)); +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/Dialog.mm b/src/InfiniFrame.Native/Platform/Mac/Dialog.mm index 80144d07b..c28d40ff5 100644 --- a/src/InfiniFrame.Native/Platform/Mac/Dialog.mm +++ b/src/InfiniFrame.Native/Platform/Mac/Dialog.mm @@ -5,6 +5,7 @@ */ #import "Core/InfiniFrameDialog.h" +#include "Interop/NativeString.h" #if defined(VSTGUI_USE_OBJC_UTTYPE) #import @@ -71,9 +72,9 @@ if ([openDlg runModal] == NSModalResponseOK) { NSArray* files = [openDlg URLs]; *resultCount = static_cast([files count]); - auto** result = static_cast(malloc(*resultCount * sizeof(char*))); + auto** result = InfiniFrame::Native::Interop::AllocateNativeStringArray(*resultCount); for (int i = 0; i < *resultCount; i++) { - result[i] = strdup([[[files objectAtIndex:i] path] UTF8String]); + result[i] = InfiniFrame::Native::Interop::AllocateNativeStringCopy([[[files objectAtIndex:i] path] UTF8String]); } return result; } @@ -95,9 +96,9 @@ if ([openDlg runModal] == NSModalResponseOK) { NSArray* files = [openDlg URLs]; *resultCount = static_cast([files count]); - auto** result = static_cast(malloc(*resultCount * sizeof(char*))); + auto** result = InfiniFrame::Native::Interop::AllocateNativeStringArray(*resultCount); for (int i = 0; i < *resultCount; i++) { - result[i] = strdup([[[files objectAtIndex:i] path] UTF8String]); + result[i] = InfiniFrame::Native::Interop::AllocateNativeStringCopy([[[files objectAtIndex:i] path] UTF8String]); } return result; } @@ -129,7 +130,7 @@ } if ([saveDlg runModal] == NSModalResponseOK) { - return strdup([[saveDlg URL].path UTF8String]); + return InfiniFrame::Native::Interop::AllocateNativeStringCopy([[saveDlg URL].path UTF8String]); } return nullptr; diff --git a/src/InfiniFrame.Native/Platform/Mac/Monitors.Cocoa.mm b/src/InfiniFrame.Native/Platform/Mac/Monitors.Cocoa.mm new file mode 100644 index 000000000..bc8360467 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/Monitors.Cocoa.mm @@ -0,0 +1,34 @@ +#ifdef __APPLE__ + +#include "Platform/Mac/WindowImpl.Cocoa.h" + +#include + +std::vector InfiniFrameWindow::Impl::GetMonitors() const +{ + std::vector monitors; + + for (NSScreen *screen : [NSScreen screens]) + { + NSRect monitorFrame = [screen frame]; + Monitor::MonitorRect monitorArea; + monitorArea.x = static_cast(roundf(monitorFrame.origin.x)); + monitorArea.y = static_cast(roundf(monitorFrame.origin.y)); + monitorArea.width = static_cast(roundf(monitorFrame.size.width)); + monitorArea.height = static_cast(roundf(monitorFrame.size.height)); + + NSRect workFrame = [screen visibleFrame]; + Monitor::MonitorRect workArea; + workArea.x = static_cast(roundf(workFrame.origin.x)); + workArea.y = static_cast(roundf(workFrame.origin.y)); + workArea.width = static_cast(roundf(workFrame.size.width)); + workArea.height = static_cast(roundf(workFrame.size.height)); + + CGFloat scaleFactor = [screen backingScaleFactor]; + monitors.push_back({monitorArea, workArea, static_cast(scaleFactor)}); + } + + return monitors; +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/Notifications.UserNotifications.Cocoa.mm b/src/InfiniFrame.Native/Platform/Mac/Notifications.UserNotifications.Cocoa.mm new file mode 100644 index 000000000..026dcac3f --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/Notifications.UserNotifications.Cocoa.mm @@ -0,0 +1,24 @@ +#ifdef __APPLE__ + +#include "Platform/Mac/WindowImpl.Cocoa.h" + +void InfiniFrameWindow::ShowNotification(AutoString title, AutoString body) +{ + UNMutableNotificationContent* notificationContent = [[[UNMutableNotificationContent alloc] init] autorelease]; + notificationContent.title = [NSString stringWithUTF8String: title == nullptr ? "" : title]; + notificationContent.body = [NSString stringWithUTF8String: body == nullptr ? "" : body]; + notificationContent.sound = [UNNotificationSound defaultSound]; + + UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger + triggerWithTimeInterval: 0.3 + repeats: NO]; + UNNotificationRequest* request = [UNNotificationRequest + requestWithIdentifier: @"three" + content: notificationContent + trigger: trigger]; + + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest: request withCompletionHandler: ^(NSError* _Nullable) {}]; +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/UiDispatcher.Cocoa.mm b/src/InfiniFrame.Native/Platform/Mac/UiDispatcher.Cocoa.mm new file mode 100644 index 000000000..f5525d308 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/UiDispatcher.Cocoa.mm @@ -0,0 +1,20 @@ +#ifdef __APPLE__ + +#include "Platform/Mac/WindowImpl.Cocoa.h" + +#include + +void InfiniFrameWindow::Invoke(ACTION callback) +{ + if (callback == nullptr) + return; + + if ([NSThread isMainThread]) + callback(); + else + dispatch_sync(dispatch_get_main_queue(), ^{ + callback(); + }); +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/UrlSchemeHandler.mm b/src/InfiniFrame.Native/Platform/Mac/UrlSchemeHandler.mm index 682eea70c..5a40eb35e 100644 --- a/src/InfiniFrame.Native/Platform/Mac/UrlSchemeHandler.mm +++ b/src/InfiniFrame.Native/Platform/Mac/UrlSchemeHandler.mm @@ -1,5 +1,6 @@ #ifdef __APPLE__ #import "UrlSchemeHandler.h" +#include "Shared/CustomSchemeResponse.h" @implementation UrlSchemeHandler : NSObject @@ -7,26 +8,17 @@ - (void)webView:(WKWebView *)webView startURLSchemeTask:(id )ur { NSURL *url = [[urlSchemeTask request] URL]; auto *urlUtf8 = const_cast([url.absoluteString UTF8String]); - int numBytes = 0; - char* contentType = nullptr; - void* dotNetResponse = requestHandler == nullptr - ? nullptr - : requestHandler(urlUtf8, &numBytes, &contentType); + auto dotNetResponse = InfiniFrame::Native::Shared::InvokeCustomSchemeCallback(requestHandler, urlUtf8); - NSInteger statusCode = dotNetResponse == nullptr ? 404 : 200; - NSString* nsContentType = contentType == nullptr - ? @"application/octet-stream" - : [[NSString stringWithUTF8String:contentType] autorelease]; + NSInteger statusCode = dotNetResponse.HasBody() ? 200 : 404; + NSString* nsContentType = [NSString stringWithUTF8String:dotNetResponse.ContentTypeOrDefault()]; NSDictionary* headers = @{ @"Content-Type" : nsContentType, @"Cache-Control": @"no-cache" }; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:statusCode HTTPVersion:nil headerFields:headers]; [urlSchemeTask didReceiveResponse:response]; - if (dotNetResponse != nullptr && numBytes > 0) - [urlSchemeTask didReceiveData:[NSData dataWithBytes:dotNetResponse length:numBytes]]; + if (dotNetResponse.HasBody() && dotNetResponse.length > 0) + [urlSchemeTask didReceiveData:[NSData dataWithBytes:dotNetResponse.body.get() length:static_cast(dotNetResponse.length)]]; [urlSchemeTask didFinish]; - - free(dotNetResponse); - free(contentType); } - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id )urlSchemeTask diff --git a/src/InfiniFrame.Native/Platform/Mac/WKCustomSchemes.Cocoa.mm b/src/InfiniFrame.Native/Platform/Mac/WKCustomSchemes.Cocoa.mm new file mode 100644 index 000000000..93d67f45e --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/WKCustomSchemes.Cocoa.mm @@ -0,0 +1,32 @@ +#ifdef __APPLE__ + +#include "Platform/Mac/WindowImpl.Cocoa.h" +#import "Platform/Mac/UrlSchemeHandler.h" + +void InfiniFrameWindow::Impl::AddCustomScheme(const AutoStringConst scheme, WebResourceRequestedCallback requestHandler) +{ + if (requestHandler == nullptr) + return; + + UrlSchemeHandler* schemeHandler = [[[UrlSchemeHandler alloc] init] autorelease]; + schemeHandler->requestHandler = requestHandler; + + [_webviewConfiguration + setURLSchemeHandler: schemeHandler + forURLScheme: [NSString stringWithUTF8String: scheme]]; +} + +void InfiniFrameWindow::Impl::AddCustomSchemeHandlers() +{ + for (const auto& scheme : _customSchemeNames) + { + // Note: + // Unlike WebView2 (Windows) and WebKitGTK (Linux security manager), + // WKURLSchemeHandler does not expose per-scheme "secure"/authority flags. + // We still register all custom schemes here for routing, but "app" trust + // semantics cannot be configured at the same granularity on macOS. + AddCustomScheme(scheme.c_str(), _customSchemeCallback); + } +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/WKJsInterop.Cocoa.mm b/src/InfiniFrame.Native/Platform/Mac/WKJsInterop.Cocoa.mm new file mode 100644 index 000000000..423e42ba4 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/WKJsInterop.Cocoa.mm @@ -0,0 +1,29 @@ +#ifdef __APPLE__ + +#include "Platform/Mac/WindowImpl.Cocoa.h" + +void InfiniFrameWindow::SendWebMessage(AutoString message) +{ + NSString* nsmessage = [NSString stringWithUTF8String: message]; + + NSData* data = [ + NSJSONSerialization + dataWithJSONObject: @[nsmessage] + options: 0 + error: nil]; + + NSString *nsmessageJson = [[ + [NSString alloc] + initWithData: data + encoding: NSUTF8StringEncoding] autorelease]; + + nsmessageJson = [ + [nsmessageJson substringToIndex: ([nsmessageJson length] - 1)] + substringFromIndex: 1 + ]; + + NSString *javaScriptToEval = [NSString stringWithFormat: @"__dispatchMessageCallback(%@)", nsmessageJson]; + [m_impl->_webview evaluateJavaScript: javaScriptToEval completionHandler: nil]; +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/WKWebViewBridge.Cocoa.mm b/src/InfiniFrame.Native/Platform/Mac/WKWebViewBridge.Cocoa.mm new file mode 100644 index 000000000..1ca6a7609 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/WKWebViewBridge.Cocoa.mm @@ -0,0 +1,78 @@ +#ifdef __APPLE__ + +#include "Platform/Mac/WindowImpl.Cocoa.h" + +#include "Embedded/Embedded.h" +#import "Platform/Mac/NavigationDelegate.h" +#import "Platform/Mac/UiDelegate.h" + +#include + +void InfiniFrameWindow::NavigateToString(AutoString content) +{ + [m_impl->_webview loadHTMLString: [NSString stringWithUTF8String: content] baseURL: nil]; +} + +void InfiniFrameWindow::NavigateToUrl(AutoString url) +{ + NSString* nsurlstring = [NSString stringWithUTF8String: url]; + NSURL *nsurl = [NSURL URLWithString: nsurlstring]; + NSURLRequest *nsrequest = [NSURLRequest requestWithURL: nsurl]; + [m_impl->_webview loadRequest: nsrequest]; +} + +void InfiniFrameWindow::CloseWebView() +{ + // Not implemented on macOS +} + +void InfiniFrameWindow::AttachWebView() +{ + if (m_impl->_startUrl.empty() && m_impl->_startString.empty()) + throw std::invalid_argument("Either StartUrl or StartString must be specified."); + + auto js = Embedded::InfiniFrameHostJsUtf8(); + + WKUserScript *script = + [[WKUserScript alloc] + initWithSource:[NSString stringWithUTF8String:js.c_str()] + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:NO]; + + WKUserContentController *userContentController = + [[WKUserContentController alloc] init]; + + [userContentController addUserScript:script]; + + m_impl->_webviewConfiguration.userContentController = userContentController; + + m_impl->_webview = [ + [WKWebView alloc] + initWithFrame: m_impl->_window.contentView.frame + configuration: m_impl->_webviewConfiguration]; + + [m_impl->_webview setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; + [m_impl->_window.contentView addSubview: m_impl->_webview]; + [m_impl->_window.contentView setAutoresizesSubviews: true]; + + m_impl->_uiDelegate = [[UiDelegate alloc] init]; + m_impl->_uiDelegate->infiniFrame = this; + m_impl->_uiDelegate->window = m_impl->_window; + m_impl->_uiDelegate->webMessageReceivedCallback = m_impl->_webMessageReceivedCallback; + + m_impl->_navigationDelegate = [[NavigationDelegate alloc] init]; + m_impl->_navigationDelegate->infiniFrame = this; + m_impl->_navigationDelegate->window = m_impl->_window; + + [userContentController addScriptMessageHandler: m_impl->_uiDelegate name: @"infiniFrameInterop"]; + + m_impl->_webview.UIDelegate = m_impl->_uiDelegate; + m_impl->_webview.navigationDelegate = m_impl->_navigationDelegate; + + if (!m_impl->_startUrl.empty()) + NavigateToUrl(const_cast(m_impl->_startUrl.c_str())); + else if (!m_impl->_startString.empty()) + NavigateToString(const_cast(m_impl->_startString.c_str())); +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/WKWebViewSettings.Cocoa.mm b/src/InfiniFrame.Native/Platform/Mac/WKWebViewSettings.Cocoa.mm new file mode 100644 index 000000000..553e9e28b --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/WKWebViewSettings.Cocoa.mm @@ -0,0 +1,102 @@ +#ifdef __APPLE__ + +#include "Core/InfiniFrameInitParams.h" +#include "Platform/Mac/WindowImpl.Cocoa.h" + +#include +#include +#include + +void InfiniFrameWindow::Impl::ConfigureWebViewPreferences(InfiniFrameInitParams* initParams) +{ + SetUserAgent(initParams->UserAgent); + + SetPreference(@"developerExtrasEnabled", initParams->DevToolsEnabled ? @YES : @NO); + SetPreference(@"allowFileAccessFromFileURLs", initParams->FileSystemAccessEnabled ? @YES : @NO); + SetPreference(@"webSecurityEnabled", initParams->WebSecurityEnabled ? @YES : @NO); + SetPreference(@"javaScriptCanAccessClipboard", initParams->JavascriptClipboardAccessEnabled ? @YES : @NO); + SetPreference(@"mediaStreamEnabled", initParams->MediaStreamEnabled ? @YES : @NO); + + SetPreference(@"mediaDevicesEnabled", @YES); + SetPreference(@"mediaCaptureRequiresSecureConnection", @NO); + + if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion: NSOperatingSystemVersion({13, 3, 0})]) + { + SetPreference(@"notificationEventEnabled", @YES); + } + + SetPreference(@"notificationsEnabled", @YES); + SetPreference(@"screenCaptureEnabled", @YES); + + if (initParams->BrowserControlInitParameters == nullptr) + return; + + simdjson::ondemand::parser parser; + auto doc = parser.iterate(initParams->BrowserControlInitParameters); + + for (auto field : doc.get_object()) { + std::string_view key = field.unescaped_key().value(); + auto value = field.value(); + + NSString *preferenceKey = [[NSString alloc] initWithBytes:key.data() length:key.length() encoding:NSUTF8StringEncoding]; + + switch (value.type()) { + case simdjson::ondemand::json_type::number: { + int64_t intVal; + if (value.get(intVal) == simdjson::SUCCESS) { + SetPreference(preferenceKey, [NSNumber numberWithInt: (int)intVal]); + } else { + double doubleVal; + if (value.get(doubleVal) == simdjson::SUCCESS) { + SetPreference(preferenceKey, [NSNumber numberWithDouble: doubleVal]); + } + } + break; + } + case simdjson::ondemand::json_type::boolean: { + bool boolVal; + if (value.get(boolVal) == simdjson::SUCCESS) { + SetPreference(preferenceKey, [NSNumber numberWithBool: boolVal]); + } + break; + } + case simdjson::ondemand::json_type::string: { + std::string_view strVal; + if (value.get(strVal) == simdjson::SUCCESS) { + NSString *preferenceValue = [[NSString alloc] initWithBytes:strVal.data() + length:strVal.length() + encoding:NSUTF8StringEncoding]; + SetPreference(preferenceKey, preferenceValue); + } + break; + } + default: + break; + } + } +} + +void InfiniFrameWindow::Impl::SetUserAgent(AutoString userAgent) +{ + if (userAgent != nullptr) + { + _userAgent = userAgent; + [_webview setCustomUserAgent: [NSString stringWithUTF8String: userAgent]]; + } + else + { + _userAgent.clear(); + } +} + +void InfiniFrameWindow::Impl::SetPreference(NSString *key, NSNumber *value) +{ + [_webviewConfiguration.preferences setValue: value forKey: key]; +} + +void InfiniFrameWindow::Impl::SetPreference(NSString *key, NSString *value) +{ + [_webviewConfiguration.preferences setValue: value forKey: key]; +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/Window.mm b/src/InfiniFrame.Native/Platform/Mac/Window.mm index 018b6b4ae..cf9d032a4 100644 --- a/src/InfiniFrame.Native/Platform/Mac/Window.mm +++ b/src/InfiniFrame.Native/Platform/Mac/Window.mm @@ -1,115 +1,17 @@ #ifdef __APPLE__ -#include "Core/InfiniFrameWindow.h" #include "Core/InfiniFrameDialog.h" -#include "Core/InfiniFrameWindowImpl.h" -#include "Embedded/Embedded.h" +#include "Interop/InitParamsReader.h" +#include "Platform/Mac/WindowImpl.Cocoa.h" #include "Utils/Common.h" #include "AppDelegate.h" -#include "UiDelegate.h" #include "WindowDelegate.h" -#include "UrlSchemeHandler.h" #include "NSWindowBorderless.h" -#include "NavigationDelegate.h" -#include -#include -using namespace std; +#include +#include static const int MAX_WINDOW_DIMENSION = 10000; -// --------------------------------------------------------------------------------------------------------------------- -// Platform Impl -// --------------------------------------------------------------------------------------------------------------------- - -struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl -{ - NSWindow* _window = nil; - WKWebView* _webview = nil; - WKWebViewConfiguration* _webviewConfiguration = nil; - - std::string _temporaryFilesPath; - - bool _chromeless = false; - - CGFloat _preMaximizedWidth = 0; - CGFloat _preMaximizedHeight = 0; - CGFloat _preMaximizedXPosition = 0; - CGFloat _preMaximizedYPosition = 0; - - std::vector GetMonitors() const; - void SetUserAgent(AutoString userAgent); - void SetPreference(NSString* key, NSNumber* value); - void SetPreference(NSString* key, NSString* value); - void AddCustomScheme(const AutoStringConst scheme, WebResourceRequestedCallback requestHandler); -}; - -// --------------------------------------------------------------------------------------------------------------------- -// Impl method definitions -// --------------------------------------------------------------------------------------------------------------------- - -std::vector InfiniFrameWindow::Impl::GetMonitors() const -{ - std::vector monitors; - - for (NSScreen *screen : [NSScreen screens]) - { - NSRect monitorFrame = [screen frame]; - Monitor::MonitorRect monitorArea; - monitorArea.x = static_cast(roundf(monitorFrame.origin.x)); - monitorArea.y = static_cast(roundf(monitorFrame.origin.y)); - monitorArea.width = static_cast(roundf(monitorFrame.size.width)); - monitorArea.height = static_cast(roundf(monitorFrame.size.height)); - - NSRect workFrame = [screen visibleFrame]; - Monitor::MonitorRect workArea; - workArea.x = static_cast(roundf(workFrame.origin.x)); - workArea.y = static_cast(roundf(workFrame.origin.y)); - workArea.width = static_cast(roundf(workFrame.size.width)); - workArea.height = static_cast(roundf(workFrame.size.height)); - - CGFloat scaleFactor = [screen backingScaleFactor]; - monitors.push_back({monitorArea, workArea, static_cast(scaleFactor)}); - } - - return monitors; -} - -void InfiniFrameWindow::Impl::SetUserAgent(AutoString userAgent) -{ - if (userAgent != nullptr) - { - _userAgent = userAgent; - [_webview setCustomUserAgent: [NSString stringWithUTF8String: userAgent]]; - } - else - { - _userAgent.clear(); - } -} - -void InfiniFrameWindow::Impl::SetPreference(NSString *key, NSNumber *value) -{ - [_webviewConfiguration.preferences setValue: value forKey: key]; -} - -void InfiniFrameWindow::Impl::SetPreference(NSString *key, NSString *value) -{ - [_webviewConfiguration.preferences setValue: value forKey: key]; -} - -void InfiniFrameWindow::Impl::AddCustomScheme(const AutoStringConst scheme, WebResourceRequestedCallback requestHandler) -{ - if (requestHandler == nullptr) - return; - - UrlSchemeHandler* schemeHandler = [[[UrlSchemeHandler alloc] init] autorelease]; - schemeHandler->requestHandler = requestHandler; - - [_webviewConfiguration - setURLSchemeHandler: schemeHandler - forURLScheme: [NSString stringWithUTF8String: scheme]]; -} - // --------------------------------------------------------------------------------------------------------------------- // Register (static — called once) // --------------------------------------------------------------------------------------------------------------------- @@ -182,6 +84,9 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : m_impl(std::make_unique()) { + const auto initParamsReader = InfiniFrame::Native::Interop::InitParamsReader(initParams); + initParamsReader.RequireStartContent(); + m_impl->_windowTitle = initParams->Title ? initParams->Title : ""; if (initParams->StartUrl != nullptr) @@ -190,6 +95,9 @@ if (initParams->StartString != nullptr) m_impl->_startString = initParams->StartString; + if (m_impl->_startUrl.empty() && m_impl->_startString.empty()) + throw std::invalid_argument("Either StartUrl or StartString must be specified."); + if (initParams->TemporaryFilesPath != nullptr) m_impl->_temporaryFilesPath = initParams->TemporaryFilesPath; @@ -265,9 +173,9 @@ [m_impl->_window setCollectionBehavior: [m_impl->_window collectionBehavior] | NSWindowCollectionBehaviorFullScreenPrimary]; - WindowDelegate *windowDelegate = [WindowDelegate new]; - windowDelegate->infiniFrame = this; - m_impl->_window.delegate = windowDelegate; + m_impl->_windowDelegate = [WindowDelegate new]; + m_impl->_windowDelegate->infiniFrame = this; + m_impl->_window.delegate = m_impl->_windowDelegate; SetTitle(const_cast(m_impl->_windowTitle.c_str())); @@ -289,84 +197,9 @@ Center(); m_impl->_webviewConfiguration = [[WKWebViewConfiguration alloc] init]; - - for (const auto & scheme : m_impl->_customSchemeNames) - { - // Note: - // Unlike WebView2 (Windows) and WebKitGTK (Linux security manager), - // WKURLSchemeHandler does not expose per-scheme "secure"/authority flags. - // We still register all custom schemes here for routing, but "app" trust - // semantics cannot be configured at the same granularity on macOS. - m_impl->AddCustomScheme(scheme.c_str(), m_impl->_customSchemeCallback); - } - + m_impl->AddCustomSchemeHandlers(); AttachWebView(); - - m_impl->SetUserAgent(initParams->UserAgent); - - m_impl->SetPreference(@"developerExtrasEnabled", initParams->DevToolsEnabled ? @YES : @NO); - m_impl->SetPreference(@"allowFileAccessFromFileURLs", initParams->FileSystemAccessEnabled ? @YES : @NO); - m_impl->SetPreference(@"webSecurityEnabled", initParams->WebSecurityEnabled ? @YES : @NO); - m_impl->SetPreference(@"javaScriptCanAccessClipboard", initParams->JavascriptClipboardAccessEnabled ? @YES : @NO); - m_impl->SetPreference(@"mediaStreamEnabled", initParams->MediaStreamEnabled ? @YES : @NO); - - m_impl->SetPreference(@"mediaDevicesEnabled", @YES); - m_impl->SetPreference(@"mediaCaptureRequiresSecureConnection", @NO); - - if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion: NSOperatingSystemVersion({13, 3, 0})]) - { - m_impl->SetPreference(@"notificationEventEnabled", @YES); - } - - m_impl->SetPreference(@"notificationsEnabled", @YES); - m_impl->SetPreference(@"screenCaptureEnabled", @YES); - - if (initParams->BrowserControlInitParameters != nullptr) - { - simdjson::ondemand::parser parser; - auto doc = parser.iterate(initParams->BrowserControlInitParameters); - - for (auto field : doc.get_object()) { - std::string_view key = field.unescaped_key().value(); - auto value = field.value(); - - NSString *preferenceKey = [[NSString alloc] initWithBytes:key.data() length:key.length() encoding:NSUTF8StringEncoding]; - - switch (value.type()) { - case simdjson::ondemand::json_type::number: { - int64_t intVal; - if (value.get(intVal) == simdjson::SUCCESS) { - m_impl->SetPreference(preferenceKey, [NSNumber numberWithInt: (int)intVal]); - } else { - double doubleVal; - if (value.get(doubleVal) == simdjson::SUCCESS) { - m_impl->SetPreference(preferenceKey, [NSNumber numberWithDouble: doubleVal]); - } - } - break; - } - case simdjson::ondemand::json_type::boolean: { - bool boolVal; - if (value.get(boolVal) == simdjson::SUCCESS) { - m_impl->SetPreference(preferenceKey, [NSNumber numberWithBool: boolVal]); - } - break; - } - case simdjson::ondemand::json_type::string: { - std::string_view strVal; - if (value.get(strVal) == simdjson::SUCCESS) { - NSString *preferenceValue = [[NSString alloc] initWithBytes:strVal.data() - length:strVal.length() - encoding:NSUTF8StringEncoding]; - m_impl->SetPreference(preferenceKey, preferenceValue); - } - break; - } - default: - break; - } - } - } + m_impl->ConfigureWebViewPreferences(initParams); m_impl->_dialog = std::make_unique(); @@ -376,6 +209,16 @@ InfiniFrameWindow::~InfiniFrameWindow() { + WKUserContentController* userContentController = m_impl->_webviewConfiguration.userContentController; + [userContentController removeScriptMessageHandlerForName: @"infiniFrameInterop"]; + + m_impl->_webview.UIDelegate = nil; + m_impl->_webview.navigationDelegate = nil; + m_impl->_window.delegate = nil; + + [m_impl->_uiDelegate release]; + [m_impl->_navigationDelegate release]; + [m_impl->_windowDelegate release]; [m_impl->_webviewConfiguration release]; [m_impl->_webview release]; [m_impl->_window performClose: m_impl->_window]; @@ -468,28 +311,6 @@ *enabled = m_impl->_mediaStreamEnabled; } -void InfiniFrameWindow::GetFullScreen(bool* fullScreen) const -{ - *fullScreen = ([m_impl->_window styleMask] & NSWindowStyleMaskFullScreen) != 0; -} - -void InfiniFrameWindow::GetMaximized(bool* isMaximized) const -{ - bool isFullScreen = false; - GetFullScreen(&isFullScreen); - if (isFullScreen) - { - *isMaximized = false; - return; - } - *isMaximized = [m_impl->_window isZoomed]; -} - -void InfiniFrameWindow::GetMinimized(bool* isMinimized) const -{ - *isMinimized = [m_impl->_window isMiniaturized]; -} - void InfiniFrameWindow::GetPosition(int* x, int* y) const { NSRect frame = [m_impl->_window frame]; @@ -573,57 +394,6 @@ return AllocateStringCopy(m_impl->_iconFileName); } -// --------------------------------------------------------------------------------------------------------------------- -// Navigation -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::NavigateToString(AutoString content) -{ - [m_impl->_webview loadHTMLString: [NSString stringWithUTF8String: content] baseURL: nil]; -} - -void InfiniFrameWindow::NavigateToUrl(AutoString url) -{ - NSString* nsurlstring = [NSString stringWithUTF8String: url]; - NSURL *nsurl = [NSURL URLWithString: nsurlstring]; - NSURLRequest *nsrequest = [NSURLRequest requestWithURL: nsurl]; - [m_impl->_webview loadRequest: nsrequest]; -} - -void InfiniFrameWindow::Restore() -{ - bool minimized; - bool maximized; - GetMinimized(&minimized); - GetMaximized(&maximized); - if (minimized) SetMinimized(false); - if (maximized) SetMaximized(false); -} - -void InfiniFrameWindow::SendWebMessage(AutoString message) -{ - NSString* nsmessage = [NSString stringWithUTF8String: message]; - - NSData* data = [ - NSJSONSerialization - dataWithJSONObject: @[nsmessage] - options: 0 - error: nil]; - - NSString *nsmessageJson = [[ - [NSString alloc] - initWithData: data - encoding: NSUTF8StringEncoding] autorelease]; - - nsmessageJson = [ - [nsmessageJson substringToIndex: ([nsmessageJson length] - 1)] - substringFromIndex: 1 - ]; - - NSString *javaScriptToEval = [NSString stringWithFormat: @"__dispatchMessageCallback(%@)", nsmessageJson]; - [m_impl->_webview evaluateJavaScript: javaScriptToEval completionHandler: nil]; -} - // --------------------------------------------------------------------------------------------------------------------- // Set Properties // --------------------------------------------------------------------------------------------------------------------- @@ -659,48 +429,6 @@ m_impl->_iconFileName = filename ? filename : ""; } -void InfiniFrameWindow::SetFullScreen(bool fullScreen) -{ - bool isFullScreen = ([m_impl->_window styleMask] & NSWindowStyleMaskFullScreen) != 0; - if (fullScreen != isFullScreen) - [m_impl->_window toggleFullScreen: nil]; -} - -void InfiniFrameWindow::SetMinimized(bool minimized) -{ - if (m_impl->_window.isMiniaturized == minimized) return; - - if (minimized) - [m_impl->_window miniaturize: nullptr]; - else - [m_impl->_window deminiaturize: nullptr]; -} - -void InfiniFrameWindow::SetMaximized(bool maximized) -{ - if (maximized) - { - NSRect window = [m_impl->_window frame]; - m_impl->_preMaximizedWidth = window.size.width; - m_impl->_preMaximizedHeight = window.size.height; - m_impl->_preMaximizedXPosition = window.origin.x; - m_impl->_preMaximizedYPosition = window.origin.y; - - NSRect screen = [[m_impl->_window screen] visibleFrame]; - [m_impl->_window setFrame: NSMakeRect(screen.origin.x, screen.origin.y, - screen.size.width, screen.size.height) - display: YES]; - } - else if (!maximized && m_impl->_preMaximizedWidth > 0 && m_impl->_preMaximizedHeight > 0) - { - [m_impl->_window setFrame: NSMakeRect(m_impl->_preMaximizedXPosition, - m_impl->_preMaximizedYPosition, - m_impl->_preMaximizedWidth, - m_impl->_preMaximizedHeight) - display: YES]; - } -} - void InfiniFrameWindow::SetPosition(int x, int y) { NSScreen* screen = [m_impl->_window screen]; @@ -790,24 +518,6 @@ } } -// --------------------------------------------------------------------------------------------------------------------- -// Notifications / Event loop -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::ShowNotification(AutoString title, AutoString body) -{ - UNMutableNotificationContent *objNotificationContent = [[UNMutableNotificationContent alloc] init]; - objNotificationContent.title = [NSString stringWithUTF8String: title]; - objNotificationContent.body = [NSString stringWithUTF8String: body]; - objNotificationContent.sound = [UNNotificationSound defaultSound]; - UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval: 0.3 repeats: NO]; - UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier: @"three" - content: objNotificationContent - trigger: trigger]; - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - [center addNotificationRequest: request withCompletionHandler: ^(NSError * _Nullable error) {}]; -} - void InfiniFrameWindow::WaitForExit() { if (![NSApp isRunning]) { @@ -832,11 +542,6 @@ [[NSNotificationCenter defaultCenter] removeObserver: observer]; } -void InfiniFrameWindow::CloseWebView() -{ - // Not implemented on macOS -} - // --------------------------------------------------------------------------------------------------------------------- // Callbacks // --------------------------------------------------------------------------------------------------------------------- @@ -854,28 +559,13 @@ void InfiniFrameWindow::GetAllMonitors(GetAllMonitorsCallback callback) const { - if (callback) + if (callback == nullptr) + return; + + for (const auto& monitor : m_impl->GetMonitors()) { - for (NSScreen* screen in [NSScreen screens]) - { - Monitor props = {}; - - NSRect frame = [screen frame]; - props.monitor.x = static_cast(roundf(frame.origin.x)); - props.monitor.y = static_cast(roundf(frame.origin.y)); - props.monitor.width = static_cast(roundf(frame.size.width)); - props.monitor.height = static_cast(roundf(frame.size.height)); - - NSRect vframe = [screen visibleFrame]; - props.work.x = static_cast(roundf(vframe.origin.x)); - props.work.y = static_cast(roundf(vframe.origin.y)); - props.work.width = static_cast(roundf(vframe.size.width)); - props.work.height = static_cast(roundf(vframe.size.height)); - - props.scale = [screen backingScaleFactor]; - - callback(&props); - } + if (!callback(&monitor)) + break; } } @@ -919,14 +609,6 @@ m_impl->_minimizedCallback = callback; } -void InfiniFrameWindow::Invoke(ACTION callback) -{ - if ([NSThread isMainThread]) - callback(); - else - dispatch_sync(dispatch_get_main_queue(), ^(void){ callback(); }); -} - [[nodiscard]] bool InfiniFrameWindow::InvokeClose() const noexcept { if (m_impl->_closingCallback) @@ -980,60 +662,10 @@ // Private methods // --------------------------------------------------------------------------------------------------------------------- -void InfiniFrameWindow::AttachWebView() -{ - auto js = Embedded::InfiniFrameHostJsUtf8(); - - WKUserScript *script = - [[WKUserScript alloc] - initWithSource:[NSString stringWithUTF8String:js.c_str()] - injectionTime:WKUserScriptInjectionTimeAtDocumentStart - forMainFrameOnly:NO]; - - WKUserContentController *userContentController = - [[WKUserContentController alloc] init]; - - [userContentController addUserScript:script]; - - m_impl->_webviewConfiguration.userContentController = userContentController; - - m_impl->_webview = [ - [WKWebView alloc] - initWithFrame: m_impl->_window.contentView.frame - configuration: m_impl->_webviewConfiguration]; - - [m_impl->_webview setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; - [m_impl->_window.contentView addSubview: m_impl->_webview]; - [m_impl->_window.contentView setAutoresizesSubviews: true]; - - UiDelegate *uiDelegate = [[[UiDelegate alloc] init] autorelease]; - uiDelegate->infiniFrame = this; - uiDelegate->window = m_impl->_window; - uiDelegate->webMessageReceivedCallback = m_impl->_webMessageReceivedCallback; - - NavigationDelegate *navDelegate = [[[NavigationDelegate alloc] init] autorelease]; - navDelegate->infiniFrame = this; - navDelegate->window = m_impl->_window; - - [userContentController addScriptMessageHandler: uiDelegate name: @"infiniFrameInterop"]; - - m_impl->_webview.UIDelegate = uiDelegate; - m_impl->_webview.navigationDelegate = navDelegate; - - if (!m_impl->_startUrl.empty()) - NavigateToUrl(const_cast(m_impl->_startUrl.c_str())); - else if (!m_impl->_startString.empty()) - NavigateToString(const_cast(m_impl->_startString.c_str())); - else - { - NSAlert *alert = [[[NSAlert alloc] init] autorelease]; - [alert setMessageText: @"Neither StartUrl nor StartString was specified"]; - [alert runModal]; - } -} - void InfiniFrameWindow::Show(bool isAlreadyShown) { + (void)isAlreadyShown; + if (m_impl->_webview == nil) AttachWebView(); diff --git a/src/InfiniFrame.Native/Platform/Mac/WindowImpl.Cocoa.h b/src/InfiniFrame.Native/Platform/Mac/WindowImpl.Cocoa.h new file mode 100644 index 000000000..a6c9f7db5 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/WindowImpl.Cocoa.h @@ -0,0 +1,44 @@ +#pragma once + +#ifdef __APPLE__ + +#include "Core/InfiniFrameWindow.h" +#include "Core/InfiniFrameWindowImpl.h" + +#include +#include + +@class NavigationDelegate; +@class UiDelegate; +@class WindowDelegate; + +struct InfiniFrameInitParams; + +struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl +{ + NSWindow* _window = nil; + WKWebView* _webview = nil; + WKWebViewConfiguration* _webviewConfiguration = nil; + NavigationDelegate* _navigationDelegate = nil; + UiDelegate* _uiDelegate = nil; + WindowDelegate* _windowDelegate = nil; + + std::string _temporaryFilesPath; + + bool _chromeless = false; + + CGFloat _preMaximizedWidth = 0; + CGFloat _preMaximizedHeight = 0; + CGFloat _preMaximizedXPosition = 0; + CGFloat _preMaximizedYPosition = 0; + + std::vector GetMonitors() const; + void ConfigureWebViewPreferences(InfiniFrameInitParams* initParams); + void SetUserAgent(AutoString userAgent); + void SetPreference(NSString* key, NSNumber* value); + void SetPreference(NSString* key, NSString* value); + void AddCustomScheme(const AutoStringConst scheme, WebResourceRequestedCallback requestHandler); + void AddCustomSchemeHandlers(); +}; + +#endif diff --git a/src/InfiniFrame.Native/Platform/Mac/WindowState.Cocoa.mm b/src/InfiniFrame.Native/Platform/Mac/WindowState.Cocoa.mm new file mode 100644 index 000000000..2abe20139 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Mac/WindowState.Cocoa.mm @@ -0,0 +1,84 @@ +#ifdef __APPLE__ + +#include "Platform/Mac/WindowImpl.Cocoa.h" + +void InfiniFrameWindow::GetFullScreen(bool* fullScreen) const +{ + *fullScreen = ([m_impl->_window styleMask] & NSWindowStyleMaskFullScreen) != 0; +} + +void InfiniFrameWindow::GetMaximized(bool* isMaximized) const +{ + bool isFullScreen = false; + GetFullScreen(&isFullScreen); + if (isFullScreen) + { + *isMaximized = false; + return; + } + + *isMaximized = [m_impl->_window isZoomed]; +} + +void InfiniFrameWindow::GetMinimized(bool* isMinimized) const +{ + *isMinimized = [m_impl->_window isMiniaturized]; +} + +void InfiniFrameWindow::Restore() +{ + bool minimized = false; + bool maximized = false; + GetMinimized(&minimized); + GetMaximized(&maximized); + + if (minimized) + SetMinimized(false); + if (maximized) + SetMaximized(false); +} + +void InfiniFrameWindow::SetFullScreen(bool fullScreen) +{ + bool isFullScreen = ([m_impl->_window styleMask] & NSWindowStyleMaskFullScreen) != 0; + if (fullScreen != isFullScreen) + [m_impl->_window toggleFullScreen: nil]; +} + +void InfiniFrameWindow::SetMinimized(bool minimized) +{ + if (m_impl->_window.isMiniaturized == minimized) + return; + + if (minimized) + [m_impl->_window miniaturize: nullptr]; + else + [m_impl->_window deminiaturize: nullptr]; +} + +void InfiniFrameWindow::SetMaximized(bool maximized) +{ + if (maximized) + { + NSRect window = [m_impl->_window frame]; + m_impl->_preMaximizedWidth = window.size.width; + m_impl->_preMaximizedHeight = window.size.height; + m_impl->_preMaximizedXPosition = window.origin.x; + m_impl->_preMaximizedYPosition = window.origin.y; + + NSRect screen = [[m_impl->_window screen] visibleFrame]; + [m_impl->_window setFrame: NSMakeRect(screen.origin.x, screen.origin.y, screen.size.width, screen.size.height) + display: YES]; + } + else if (m_impl->_preMaximizedWidth > 0 && m_impl->_preMaximizedHeight > 0) + { + [m_impl->_window setFrame: NSMakeRect( + m_impl->_preMaximizedXPosition, + m_impl->_preMaximizedYPosition, + m_impl->_preMaximizedWidth, + m_impl->_preMaximizedHeight) + display: YES]; + } +} + +#endif diff --git a/src/InfiniFrame.Native/Platform/Windows/Dialog.cpp b/src/InfiniFrame.Native/Platform/Windows/Dialog.cpp index 6a9d481dd..90eedc8d3 100644 --- a/src/InfiniFrame.Native/Platform/Windows/Dialog.cpp +++ b/src/InfiniFrame.Native/Platform/Windows/Dialog.cpp @@ -236,7 +236,7 @@ AutoString* GetResults(IFileOpenDialog* pfd, HRESULT* hr, int* resultCount) { psiResults->GetCount(&count); if (count > 0) { *resultCount = static_cast(count); - auto** result = new wchar_t*[count](); + auto** result = InfiniFrame::Native::Interop::AllocateNativeStringArray(*resultCount); for (DWORD i = 0; i < count; ++i) { IShellItem* psiItem = nullptr; *hr = psiResults->GetItemAt(i, &psiItem); @@ -244,9 +244,7 @@ AutoString* GetResults(IFileOpenDialog* pfd, HRESULT* hr, int* resultCount) { PWSTR pszName = nullptr; *hr = psiItem->GetDisplayName(SIGDN_FILESYSPATH, &pszName); if (SUCCEEDED(*hr)) { - const auto len = wcslen(pszName); - result[i] = new wchar_t[len + 1]; - wcscpy_s(result[i], len + 1, pszName); + result[i] = InfiniFrame::Native::Interop::AllocateNativeStringCopy(pszName); CoTaskMemFree(pszName); } psiItem->Release(); @@ -368,9 +366,7 @@ AutoString InfiniFrameDialog::ShowSaveFile( PWSTR pszName = nullptr; hr = psiResult->GetDisplayName(SIGDN_FILESYSPATH, &pszName); if (SUCCEEDED(hr)) { - const auto len = wcslen(pszName); - result = new wchar_t[len + 1]; - wcscpy_s(result, len + 1, pszName); + result = InfiniFrame::Native::Interop::AllocateNativeStringCopy(pszName); CoTaskMemFree(pszName); } psiResult->Release(); diff --git a/src/InfiniFrame.Native/Platform/Windows/Monitors.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/Monitors.Win32.cpp new file mode 100644 index 000000000..9192ea225 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/Monitors.Win32.cpp @@ -0,0 +1,44 @@ +#include "WindowImpl.Win32.h" + +#include + +namespace { + BOOL CALLBACK MonitorEnum(const HMONITOR monitor, HDC, LPRECT, const LPARAM arg) { + auto callback = reinterpret_cast(arg); + UINT dpiX = 96; + UINT dpiY = 96; + MONITORINFO info = {}; + info.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(monitor, &info); + GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); + + Monitor props = {}; + props.monitor.x = info.rcMonitor.left; + props.monitor.y = info.rcMonitor.top; + props.monitor.width = info.rcMonitor.right - info.rcMonitor.left; + props.monitor.height = info.rcMonitor.bottom - info.rcMonitor.top; + props.work.x = info.rcWork.left; + props.work.y = info.rcWork.top; + props.work.width = info.rcWork.right - info.rcWork.left; + props.work.height = info.rcWork.bottom - info.rcWork.top; + props.scale = dpiY / 96.0; + + return callback(&props) ? TRUE : FALSE; + } +} + +unsigned int InfiniFrameWindow::GetScreenDpi() const { + return GetDpiForWindow(m_impl->_hWnd); +} + +void InfiniFrameWindow::GetAllMonitors(GetAllMonitorsCallback callback) const { + if (callback == nullptr) + return; + + EnumDisplayMonitors( + nullptr, + nullptr, + MonitorEnum, + reinterpret_cast(callback) + ); +} diff --git a/src/InfiniFrame.Native/Platform/Windows/Notifications.WinToast.cpp b/src/InfiniFrame.Native/Platform/Windows/Notifications.WinToast.cpp new file mode 100644 index 000000000..635892835 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/Notifications.WinToast.cpp @@ -0,0 +1,42 @@ +#include "WindowImpl.Win32.h" + +#include "Dependencies/wintoastlib/wintoastlib.h" + +using namespace WinToastLib; + +void InfiniFrameWindow::Impl::ConfigureNotificationIdentityForTitle(const std::wstring& title) { + if (!_notificationsEnabled || title.empty()) + return; + + WinToast::instance()->setAppName(title.c_str()); + if (_notificationRegistrationId.empty()) + WinToast::instance()->setAppUserModelId(title.c_str()); +} + +void InfiniFrameWindow::Impl::InitializeNotifications(InfiniFrameWindow* window) { + if (!_notificationsEnabled) + return; + + if (!_notificationRegistrationId.empty()) + WinToast::instance()->setAppUserModelId(_notificationRegistrationId.c_str()); + + _toastHandler = std::make_unique(window); + WinToast::instance()->initialize(); +} + +void InfiniFrameWindow::GetNotificationsEnabled(bool* enabled) const { + *enabled = m_impl->_notificationsEnabled; +} + +void InfiniFrameWindow::ShowNotification(AutoString title, AutoString body) { + std::wstring wideTitle = ToUTF16String(title); + std::wstring wideBody = ToUTF16String(body); + if (m_impl->_notificationsEnabled && WinToast::isCompatible()) { + WinToastTemplate toast = WinToastTemplate(WinToastTemplate::ImageAndText02); + toast.setTextField(wideTitle.c_str(), WinToastTemplate::FirstLine); + toast.setTextField(wideBody.c_str(), WinToastTemplate::SecondLine); + if (!m_impl->_iconFileName.empty()) + toast.setImagePath(m_impl->_iconFileName); + WinToast::instance()->showToast(toast, m_impl->_toastHandler.get()); + } +} diff --git a/src/InfiniFrame.Native/Platform/Windows/UiDispatcher.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/UiDispatcher.Win32.cpp new file mode 100644 index 000000000..bdd772000 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/UiDispatcher.Win32.cpp @@ -0,0 +1,34 @@ +#include "WindowImpl.Win32.h" +#include "WindowProc.Win32.h" + +#include +#include + +void InfiniFrameWindow::Invoke(ACTION callback) { + if (callback == nullptr) + return; + + if (m_impl->_hWnd == nullptr || !IsWindow(m_impl->_hWnd)) + return; + + InfiniFrame::Platform::Windows::InvokeWaitInfo waitInfo = {}; + if (!PostMessage( + m_impl->_hWnd, + InfiniFrame::Platform::Windows::InvokeMessage, + reinterpret_cast(callback), + reinterpret_cast(&waitInfo) + )) + return; + + std::unique_lock uLock(InfiniFrame::Platform::Windows::InvokeLockMutex); + const bool completed = waitInfo.completionNotifier.wait_for( + uLock, + std::chrono::seconds(15), + [&] { + return waitInfo.isCompleted; + } + ); + + if (!completed) + OutputDebugStringW(L"InfiniFrameWindow::Invoke timed out waiting for UI thread callback.\n"); +} diff --git a/src/InfiniFrame.Native/Platform/Windows/WebView2Bridge.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/WebView2Bridge.Win32.cpp new file mode 100644 index 000000000..5482a4f93 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WebView2Bridge.Win32.cpp @@ -0,0 +1,64 @@ +#include "WindowImpl.Win32.h" + +#include +#include +#include + +#include +#include + +#include "Embedded/Embedded.h" + +using Microsoft::WRL::Callback; + +void InfiniFrameWindow::Impl::NavigateToInitialContent() { + if (!_startUrl.empty()) { + _webviewWindow->Navigate(_startUrl.c_str()); + } + else if (!_startString.empty()) { + _webviewWindow->NavigateToString(_startString.c_str()); + } + else { + throw std::invalid_argument("Either StartUrl or StartString must be specified."); + } +} + +void InfiniFrameWindow::Impl::RegisterBridgeScriptAndNavigate() { + const auto js_wide = Embedded::InfiniFrameHostJsUtf16(); + OutputDebugStringW(std::format(L"[InfiniFrame] Bridge script length: {} chars\n", js_wide.size()).c_str()); + + // AddScriptToExecuteOnDocumentCreated is async; navigating before the callback can load + // app://localhost content before window.external.receiveMessage exists. + struct NavigateOnce { + InfiniFrameWindow::Impl* impl; + bool fired = false; + + void navigate() { + if (fired) + return; + + fired = true; + impl->NavigateToInitialContent(); + } + }; + auto nav = std::make_shared(NavigateOnce{this}); + + HRESULT addScriptHr = _webviewWindow->AddScriptToExecuteOnDocumentCreated( + js_wide.c_str(), + Callback( + [nav](HRESULT errorCode, LPCWSTR id) -> HRESULT { + OutputDebugStringW(std::format( + L"[InfiniFrame] AddScriptToExecuteOnDocumentCreated callback: hr=0x{:08X} id={}\n", + static_cast(errorCode), + id ? id : L"(null)" + ).c_str()); + nav->navigate(); + return S_OK; + } + ).Get() + ); + + // If script registration fails synchronously, navigate anyway so the page is not left blank. + if (FAILED(addScriptHr)) + nav->navigate(); +} diff --git a/src/InfiniFrame.Native/Platform/Windows/WebView2CustomSchemes.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/WebView2CustomSchemes.Win32.cpp new file mode 100644 index 000000000..cdb298397 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WebView2CustomSchemes.Win32.cpp @@ -0,0 +1,67 @@ +#include "WebView2CustomSchemes.Win32.h" + +#include + +#include +#include +#include +#include + +namespace { + bool IsAppScheme(const std::wstring& schemeName) { + return _wcsicmp(schemeName.c_str(), L"app") == 0; + } + + bool RequiresAppSchemeRegistration(const std::vector& customSchemeNames) { + return std::any_of(customSchemeNames.begin(), customSchemeNames.end(), IsAppScheme); + } +} + +namespace InfiniFrame::Platform::Windows { + bool TryRegisterCustomSchemes( + ICoreWebView2EnvironmentOptions* options, + const std::vector& customSchemeNames + ) { + const bool requiresAppSchemeRegistration = RequiresAppSchemeRegistration(customSchemeNames); + bool customSchemeRegistrationSupported = false; + + if (!customSchemeNames.empty() && options != nullptr) { + wil::com_ptr options4; + if (SUCCEEDED(options->QueryInterface(IID_PPV_ARGS(&options4))) && options4) { + customSchemeRegistrationSupported = true; + + std::vector> registrations; + registrations.reserve(customSchemeNames.size()); + + for (const auto& schemeName : customSchemeNames) { + auto registration = Microsoft::WRL::Make(schemeName.c_str()); + if (!registration) + continue; + + // app://localhost/... backs embedded assets and needs secure, authority-bearing navigation. + if (IsAppScheme(schemeName)) { + registration->put_HasAuthorityComponent(TRUE); + registration->put_TreatAsSecure(TRUE); + } + + registrations.emplace_back(registration); + } + + if (!registrations.empty()) { + std::vector rawRegistrations; + rawRegistrations.reserve(registrations.size()); + + for (auto& registration : registrations) + rawRegistrations.emplace_back(registration.get()); + + options4->SetCustomSchemeRegistrations( + static_cast(rawRegistrations.size()), + rawRegistrations.data() + ); + } + } + } + + return !requiresAppSchemeRegistration || customSchemeRegistrationSupported; + } +} diff --git a/src/InfiniFrame.Native/Platform/Windows/WebView2CustomSchemes.Win32.h b/src/InfiniFrame.Native/Platform/Windows/WebView2CustomSchemes.Win32.h new file mode 100644 index 000000000..3bc600f65 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WebView2CustomSchemes.Win32.h @@ -0,0 +1,22 @@ +#pragma once +/** + * @file WebView2CustomSchemes.Win32.h + * @brief WebView2 custom scheme registration helpers. + */ + +#ifndef INFINIFRAME_PLATFORM_WINDOWS_WEBVIEW2CUSTOMSCHEMES_WIN32_H +#define INFINIFRAME_PLATFORM_WINDOWS_WEBVIEW2CUSTOMSCHEMES_WIN32_H + +#include +#include + +#include + +namespace InfiniFrame::Platform::Windows { + bool TryRegisterCustomSchemes( + ICoreWebView2EnvironmentOptions* options, + const std::vector& customSchemeNames + ); +} + +#endif // INFINIFRAME_PLATFORM_WINDOWS_WEBVIEW2CUSTOMSCHEMES_WIN32_H diff --git a/src/InfiniFrame.Native/Platform/Windows/WebView2Host.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/WebView2Host.Win32.cpp new file mode 100644 index 000000000..e9bac022a --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WebView2Host.Win32.cpp @@ -0,0 +1,288 @@ +#include "WindowImpl.Win32.h" + +#include "WebView2CustomSchemes.Win32.h" + +#include +#include +#include +#include + +#include +#include +#include + +using Microsoft::WRL::Callback; + +extern wchar_t _webview2RuntimePath[MAX_PATH]; + +namespace { + std::string WideToUtf8(const wchar_t* source) { + if (source == nullptr) + return {}; + + const size_t utf16Length = wcslen(source); + if (utf16Length == 0) + return {}; + + const auto* utf16 = reinterpret_cast(source); + if (const auto validation = simdutf::validate_utf16_with_errors(utf16, utf16Length); validation.is_err()) + return {}; + + std::string utf8(simdutf::utf8_length_from_utf16(utf16, utf16Length), '\0'); + const size_t written = simdutf::convert_valid_utf16_to_utf8( + utf16, + utf16Length, + utf8.data() + ); + utf8.resize(written); + + return utf8; + } + + std::wstring DescribeHResult(const HRESULT result, const wchar_t* stage) { + _com_error error(result); + return std::format( + L"{} failed with HRESULT 0x{:08X}: {}", + stage ? stage : L"WebView2 initialization", + static_cast(result), + error.ErrorMessage() + ); + } +} + +void InfiniFrameWindow::AttachWebView() { + size_t runtimePathLen = wcsnlen(_webview2RuntimePath, _countof(_webview2RuntimePath)); + PCWSTR runtimePath = runtimePathLen > 0 ? &_webview2RuntimePath[0] : nullptr; + + m_impl->_isWebView2Initializing = true; + m_impl->_isInitialized = false; + m_impl->_webviewInitializationFailed = false; + m_impl->_webviewInitializationResult = S_OK; + m_impl->_webviewInitializationError.clear(); + + std::wstring startupString; + if (!m_impl->_userAgent.empty()) + startupString += L"--user-agent=\"" + m_impl->_userAgent + L"\" "; + if (m_impl->_mediaAutoplayEnabled) + startupString += L"--autoplay-policy=no-user-gesture-required "; + if (m_impl->_fileSystemAccessEnabled) + startupString += L"--allow-file-access-from-files "; + if (!m_impl->_webSecurityEnabled) + startupString += L"--disable-web-security "; + if (m_impl->_javascriptClipboardAccessEnabled) + startupString += L"--enable-javascript-clipboard-access "; + if (m_impl->_mediaStreamEnabled) + startupString += L"--enable-usermedia-screen-capturing "; + if (!m_impl->_smoothScrollingEnabled) + startupString += L"--disable-smooth-scrolling "; + if (m_impl->_ignoreCertificateErrorsEnabled) + startupString += L"--ignore-certificate-errors "; + if (!m_impl->_browserControlInitParameters.empty()) + startupString += m_impl->_browserControlInitParameters; + + auto options = Microsoft::WRL::Make(); + if (startupString.length() > 0) + options->put_AdditionalBrowserArguments(startupString.c_str()); + + if (!InfiniFrame::Platform::Windows::TryRegisterCustomSchemes(options.Get(), m_impl->_customSchemeNames)) { + throw std::runtime_error( + "This app requires WebView2 custom scheme registration for app://localhost/. " + "Please update WebView2 Runtime to a version that supports ICoreWebView2EnvironmentOptions4." + ); + } + + HRESULT envResult = CreateCoreWebView2EnvironmentWithOptions( + runtimePath, + m_impl->_temporaryFilesPath.empty() ? nullptr : m_impl->_temporaryFilesPath.c_str(), + options.Get(), + Callback( + [&](const HRESULT result, ICoreWebView2Environment* env) -> HRESULT { + if (FAILED(result) || env == nullptr) { + m_impl->FailWebViewInitialization( + FAILED(result) ? result : E_POINTER, + L"CreateCoreWebView2EnvironmentWithOptions" + ); + return S_OK; + } + + HRESULT envResult = env->QueryInterface(&m_impl->_webviewEnvironment); + if (FAILED(envResult)) { + m_impl->FailWebViewInitialization(envResult, L"ICoreWebView2Environment QueryInterface"); + return S_OK; + } + + const HRESULT controllerStartResult = env->CreateCoreWebView2Controller( + m_impl->_hWnd, + Callback( + [&](const HRESULT result, ICoreWebView2Controller* controller) -> HRESULT { + if (FAILED(result) || controller == nullptr) { + m_impl->FailWebViewInitialization( + FAILED(result) ? result : E_POINTER, + L"CreateCoreWebView2Controller" + ); + return S_OK; + } + + HRESULT controllerResult = controller->QueryInterface(&m_impl->_webviewController); + if (FAILED(controllerResult)) { + m_impl->FailWebViewInitialization( + controllerResult, + L"ICoreWebView2Controller QueryInterface" + ); + return S_OK; + } + + const HRESULT coreWebViewResult = m_impl->_webviewController->get_CoreWebView2( + &m_impl->_webviewWindow + ); + if (FAILED(coreWebViewResult) || !m_impl->_webviewWindow) { + m_impl->FailWebViewInitialization( + FAILED(coreWebViewResult) ? coreWebViewResult : E_POINTER, + L"ICoreWebView2Controller::get_CoreWebView2" + ); + return S_OK; + } + + m_impl->RegisterBridgeScriptAndNavigate(); + + HRESULT settingsResult = m_impl->ConfigureWebViewSettings(); + if (FAILED(settingsResult)) { + m_impl->FailWebViewInitialization(settingsResult, L"ConfigureWebViewSettings"); + return S_OK; + } + + m_impl->RegisterWebMessageReceivedHandler(); + m_impl->RegisterWebResourceRequestedHandler(); + m_impl->RegisterPermissionRequestedHandler(); + + if (!m_impl->_contextMenuEnabled) + SetContextMenuEnabled(false); + if (!m_impl->_zoomEnabled) + SetZoomEnabled(false); + if (!m_impl->_devToolsEnabled) + SetDevToolsEnabled(false); + if (m_impl->_transparentEnabled) + SetTransparentEnabled(true); + if (m_impl->_zoom != 100) + SetZoom(m_impl->_zoom); + + RefitContent(); + FocusWebView2(); + + if (m_impl->_topmost) + SetTopmost(true); + + m_impl->MarkWebViewInitialized(); + return S_OK; + } + ).Get() + ); + if (FAILED(controllerStartResult)) + m_impl->FailWebViewInitialization(controllerStartResult, L"CreateCoreWebView2Controller"); + return S_OK; + } + ).Get() + ); + + if (envResult != S_OK) { + m_impl->_isWebView2Initializing = false; + _com_error err(envResult); + throw std::runtime_error(WideToUtf8(err.ErrorMessage())); + } + + m_impl->WaitForWebViewInitialization(); +} + +void InfiniFrameWindow::Impl::FailWebViewInitialization(const HRESULT result, const wchar_t* stage) noexcept { + if (_webviewInitializationFailed) + return; + + _webviewInitializationFailed = true; + _isInitialized = false; + _isWebView2Initializing = false; + _webviewInitializationResult = result; + + try { + _webviewInitializationError = DescribeHResult(result, stage); + } + catch (...) { + _webviewInitializationError = L"WebView2 initialization failed."; + } + + OutputDebugStringW((L"[InfiniFrame] " + _webviewInitializationError + L"\n").c_str()); + + if (_hWnd != nullptr && IsWindow(_hWnd)) + DestroyWindow(_hWnd); +} + +void InfiniFrameWindow::Impl::MarkWebViewInitialized() noexcept { + _isInitialized = true; + _isWebView2Initializing = false; + _webviewInitializationFailed = false; + _webviewInitializationResult = S_OK; + _webviewInitializationError.clear(); +} + +void InfiniFrameWindow::Impl::ThrowIfWebViewInitializationFailed() const { + if (!_webviewInitializationFailed) + return; + + throw std::runtime_error(WideToUtf8(_webviewInitializationError.c_str())); +} + +void InfiniFrameWindow::Impl::WaitForWebViewInitialization() { + constexpr auto initializationTimeout = std::chrono::seconds(30); + const auto deadline = std::chrono::steady_clock::now() + initializationTimeout; + + while (_isWebView2Initializing && !_webviewInitializationFailed) { + MSG msg = {}; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + PostQuitMessage(static_cast(msg.wParam)); + FailWebViewInitialization(HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED), L"WebView2 initialization"); + break; + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + + if (!_isWebView2Initializing || _webviewInitializationFailed) + break; + } + + if (!_isWebView2Initializing || _webviewInitializationFailed) + break; + + if (std::chrono::steady_clock::now() >= deadline) { + FailWebViewInitialization(HRESULT_FROM_WIN32(WAIT_TIMEOUT), L"WebView2 initialization timeout"); + break; + } + + MsgWaitForMultipleObjectsEx(0, nullptr, 50, QS_ALLINPUT, MWMO_INPUTAVAILABLE); + } + + ThrowIfWebViewInitializationFailed(); +} + +void InfiniFrameWindow::Impl::UnregisterWebViewEventHandlers() noexcept { + if (_webviewWindow == nullptr) + return; + + if (_permissionRequestedRegistered) { + _webviewWindow->remove_PermissionRequested(_permissionRequestedToken); + _permissionRequestedRegistered = false; + _permissionRequestedToken = {}; + } + + if (_webResourceRequestedRegistered) { + _webviewWindow->remove_WebResourceRequested(_webResourceRequestedTokenForCustomScheme); + _webResourceRequestedRegistered = false; + _webResourceRequestedTokenForCustomScheme = {}; + } + + if (_webMessageReceivedRegistered) { + _webviewWindow->remove_WebMessageReceived(_webMessageReceivedToken); + _webMessageReceivedRegistered = false; + _webMessageReceivedToken = {}; + } +} diff --git a/src/InfiniFrame.Native/Platform/Windows/WebView2Messaging.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/WebView2Messaging.Win32.cpp new file mode 100644 index 000000000..2c3f11b35 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WebView2Messaging.Win32.cpp @@ -0,0 +1,39 @@ +#include "WindowImpl.Win32.h" + +#include +#include + +using Microsoft::WRL::Callback; + +void InfiniFrameWindow::Impl::RegisterWebMessageReceivedHandler() { + const HRESULT result = _webviewWindow->add_WebMessageReceived( + Callback( + [this]( + ICoreWebView2*, + ICoreWebView2WebMessageReceivedEventArgs* args + ) -> HRESULT { + return HandleWebMessageReceived(args); + } + ).Get(), + &_webMessageReceivedToken + ); + _webMessageReceivedRegistered = SUCCEEDED(result); +} + +HRESULT InfiniFrameWindow::Impl::HandleWebMessageReceived( + ICoreWebView2WebMessageReceivedEventArgs* args + ) { + if (_webMessageReceivedCallback == nullptr) + return S_OK; + + wil::unique_cotaskmem_string message; + wil::unique_cotaskmem_string source; + args->TryGetWebMessageAsString(&message); + args->get_Source(&source); + + if ((source.get() == nullptr || source.get()[0] == L'\0') && _webviewWindow != nullptr) + _webviewWindow->get_Source(&source); + + _webMessageReceivedCallback(message.get(), source.get()); + return S_OK; +} diff --git a/src/InfiniFrame.Native/Platform/Windows/WebView2ResourceRequests.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/WebView2ResourceRequests.Win32.cpp new file mode 100644 index 000000000..a44bc7ab0 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WebView2ResourceRequests.Win32.cpp @@ -0,0 +1,149 @@ +#include "WindowImpl.Win32.h" + +#include "Shared/CustomSchemeResponse.h" + +#include +#include +#include + +#include +#include +#include + +using Microsoft::WRL::Callback; + +namespace { + constexpr BYTE EmptyBlazorModuleArray[] = {'[', ']'}; + constexpr wchar_t BlazorModulesJsonPath[] = L"/_framework/blazor.modules.json"; + + std::wstring GetOriginHeader(ICoreWebView2WebResourceRequest* request) { + wil::com_ptr requestHeaders; + if (FAILED(request->get_Headers(&requestHeaders)) || !requestHeaders) + return {}; + + wil::unique_cotaskmem_string originHeaderValue; + if (FAILED(requestHeaders->GetHeader(L"Origin", &originHeaderValue)) + || originHeaderValue.get() == nullptr + || originHeaderValue.get()[0] == L'\0') + return {}; + + return originHeaderValue.get(); + } + + HRESULT PutBytesResponse( + ICoreWebView2Environment* environment, + ICoreWebView2WebResourceRequestedEventArgs* args, + const BYTE* data, + const int numBytes, + const std::wstring_view contentType, + const std::wstring_view requestOrigin + ) { + if (environment == nullptr || args == nullptr || data == nullptr || numBytes < 0) + return S_OK; + + wil::com_ptr dataStream; + dataStream.attach(SHCreateMemStream(data, static_cast(numBytes))); + if (!dataStream) + return S_OK; + + wil::com_ptr response; + const std::wstring responseHeaders = InfiniFrame::Native::Shared::BuildCorsResponseHeaders( + contentType, + requestOrigin + ); + if (SUCCEEDED(environment->CreateWebResourceResponse( + dataStream.get(), + 200, + L"OK", + responseHeaders.c_str(), + &response + )) + && response) { + args->put_Response(response.get()); + } + + return S_OK; + } +} + +void InfiniFrameWindow::Impl::RegisterWebResourceRequestedHandler() { + auto webview23 = _webviewWindow.try_query(); + if (webview23) { + webview23->AddWebResourceRequestedFilterWithRequestSourceKinds( + L"*", + COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL, + COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL + ); + } + else { + _webviewWindow->AddWebResourceRequestedFilter( + L"*", + COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL + ); + } + + const HRESULT result = _webviewWindow->add_WebResourceRequested( + Callback( + [this]( + ICoreWebView2*, + ICoreWebView2WebResourceRequestedEventArgs* args + ) -> HRESULT { + return HandleWebResourceRequested(args); + } + ).Get(), + &_webResourceRequestedTokenForCustomScheme + ); + _webResourceRequestedRegistered = SUCCEEDED(result); +} + +HRESULT InfiniFrameWindow::Impl::HandleWebResourceRequested( + ICoreWebView2WebResourceRequestedEventArgs* args + ) { + wil::com_ptr request; + if (FAILED(args->get_Request(&request)) || !request) + return S_OK; + + wil::unique_cotaskmem_string uri; + if (FAILED(request->get_Uri(&uri)) || uri.get() == nullptr) + return S_OK; + + std::wstring uriString = uri.get(); + const std::wstring requestOrigin = GetOriginHeader(request.get()); + + if (uriString.find(BlazorModulesJsonPath) != std::wstring::npos) { + return PutBytesResponse( + _webviewEnvironment.get(), + args, + EmptyBlazorModuleArray, + static_cast(sizeof(EmptyBlazorModuleArray)), + InfiniFrame::Native::Shared::JsonCustomSchemeContentType, + requestOrigin + ); + } + + const size_t colonPos = uriString.find(L':', 0); + if (colonPos == std::wstring::npos || colonPos == 0) + return S_OK; + + const std::wstring scheme = uriString.substr(0, colonPos); + const auto it = std::find(_customSchemeNames.begin(), _customSchemeNames.end(), scheme); + if (it == _customSchemeNames.end() || _customSchemeCallback == nullptr) + return S_OK; + + auto dotNetResponse = InfiniFrame::Native::Shared::InvokeCustomSchemeCallback( + _customSchemeCallback, + const_cast(uriString.c_str()) + ); + + if (!dotNetResponse.HasBody()) + return S_OK; + + return PutBytesResponse( + _webviewEnvironment.get(), + args, + reinterpret_cast(dotNetResponse.body.get()), + dotNetResponse.length, + dotNetResponse.ContentTypeOrDefault(), + requestOrigin + ); +} diff --git a/src/InfiniFrame.Native/Platform/Windows/WebView2Settings.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/WebView2Settings.Win32.cpp new file mode 100644 index 000000000..967e4a6b3 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WebView2Settings.Win32.cpp @@ -0,0 +1,38 @@ +#include "WindowImpl.Win32.h" + +#include +#include + +using Microsoft::WRL::Callback; + +HRESULT InfiniFrameWindow::Impl::ConfigureWebViewSettings() const { + wil::com_ptr settings; + HRESULT settingsResult = _webviewWindow->get_Settings(&settings); + if (FAILED(settingsResult) || !settings) + return FAILED(settingsResult) ? settingsResult : E_FAIL; + + settings->put_AreHostObjectsAllowed(TRUE); + settings->put_IsScriptEnabled(TRUE); + settings->put_AreDefaultScriptDialogsEnabled(TRUE); + settings->put_IsWebMessageEnabled(TRUE); + + return S_OK; +} + +void InfiniFrameWindow::Impl::RegisterPermissionRequestedHandler() { + const HRESULT result = _webviewWindow->add_PermissionRequested( + Callback( + [this]( + ICoreWebView2*, + ICoreWebView2PermissionRequestedEventArgs* args + ) -> HRESULT { + if (_grantBrowserPermissions) + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + + return S_OK; + } + ).Get(), + &_permissionRequestedToken + ); + _permissionRequestedRegistered = SUCCEEDED(result); +} diff --git a/src/InfiniFrame.Native/Platform/Windows/Window.cpp b/src/InfiniFrame.Native/Platform/Windows/Window.cpp index 18797937f..6f51017d6 100644 --- a/src/InfiniFrame.Native/Platform/Windows/Window.cpp +++ b/src/InfiniFrame.Native/Platform/Windows/Window.cpp @@ -1,89 +1,27 @@ -#include -#include -#include -#include #include -#include #include -#include -#include #include #include #include -#include +#include #include "Core/InfiniFrameDialog.h" -#include "Core/InfiniFrameWindow.h" -#include "Core/InfiniFrameWindowImpl.h" #include #include "DarkMode.h" -#include "ToastHandler.h" +#include "Interop/InitParamsReader.h" +#include "WindowImpl.Win32.h" +#include "WindowProc.Win32.h" #include "Utils/Common.h" -#include "Embedded/Embedded.h" - #pragma comment(lib, "Shcore.lib") #pragma comment(lib, "Urlmon.lib") -#define WM_USER_INVOKE (WM_USER + 0x0002) - -using namespace WinToastLib; using namespace Microsoft::WRL; -// --------------------------------------------------------------------------------------------------------------------- -// InfiniFrameWindow::Impl definition -// --------------------------------------------------------------------------------------------------------------------- - -struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { - std::wstring _temporaryFilesPath; - std::wstring _notificationRegistrationId; - - bool _notificationsEnabled = false; - bool _isInitialized = false; - bool _isWebView2Initializing = false; - bool _centerOnInitialize = false; - bool _chromeless = false; - bool _fullScreen = false; - bool _maximized = false; - bool _minimized = false; - bool _resizable = true; - bool _topmost = false; - bool _useOsDefaultLocation = false; - bool _useOsDefaultSize = false; - bool _hasSavedRect = false; - - RECT _savedRect = {}; - - int _zoom = 100; - int _minWidth = MinWindowDimension; - int _minHeight = MinWindowDimension; - int _maxWidth = MaxWindowDimension; - int _maxHeight = MaxWindowDimension; - - HWND _hWnd = nullptr; - wil::com_ptr _webviewController; - wil::com_ptr _webviewWindow; - wil::com_ptr _webviewEnvironment; - - EventRegistrationToken _webMessageReceivedToken = {}; - EventRegistrationToken _webResourceRequestedTokenForCustomScheme = {}; - EventRegistrationToken _windowClosedToken = {}; - EventRegistrationToken _windowClosingToken = {}; - EventRegistrationToken _documentTitleChangedToken = {}; - EventRegistrationToken _coreWebView2InitializedToken = {}; - - std::unique_ptr _toastHandler; -}; - -LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); auto CLASS_NAME = L"InfiniFrame"; -std::mutex invokeLockMutex; -std::mutex hwndMapMutex; HINSTANCE _hInstance; -thread_local HWND messageLoopRootWindowHandle = nullptr; wchar_t _webview2RuntimePath[MAX_PATH]; -std::map hwndToInfiniFrame; namespace { static_assert(sizeof(wchar_t) == sizeof(char16_t)); @@ -136,13 +74,9 @@ namespace { return utf8; } -} +} -struct InvokeWaitInfo { - std::condition_variable completionNotifier; - bool isCompleted; -}; struct ShowMessageParams { std::wstring title; @@ -150,42 +84,6 @@ struct ShowMessageParams { UINT type = 0; }; -namespace detail { - class BrushManager { - public: - static BrushManager& instance() noexcept { - static BrushManager inst; - return inst; - } - - HBRUSH dark() const noexcept { - return static_cast(m_darkBrush.get()); - } - - HBRUSH light() const noexcept { - return static_cast(m_lightBrush.get()); - } - - private: - BrushManager() noexcept { - m_darkBrush.reset(CreateSolidBrush(RGB(0, 0, 0))); - m_lightBrush.reset(CreateSolidBrush(RGB(255, 255, 255))); - } - - ~BrushManager() noexcept = default; - - struct HBRUSHDeleter { - void operator()(void* h) const noexcept { - if (h) - DeleteObject(static_cast(h)); - } - }; - - std::unique_ptr m_darkBrush; - std::unique_ptr m_lightBrush; - }; -} // namespace detail - void InfiniFrameWindow::Register(const HINSTANCE hInstance) { InitDarkModeSupport(); @@ -202,8 +100,8 @@ void InfiniFrameWindow::Register(const HINSTANCE hInstance) { wcx.hIcon = LoadIcon(hInstance, IDI_APPLICATION); wcx.hCursor = LoadCursor(nullptr, IDC_ARROW); wcx.hbrBackground = IsDarkModeEnabled() - ? detail::BrushManager::instance().dark() - : detail::BrushManager::instance().light(); + ? InfiniFrame::Platform::Windows::DarkBackgroundBrush() + : InfiniFrame::Platform::Windows::LightBackgroundBrush(); wcx.lpszMenuName = nullptr; wcx.lpszClassName = CLASS_NAME; wcx.hIconSm = LoadIcon(hInstance, IDI_APPLICATION); @@ -214,24 +112,13 @@ void InfiniFrameWindow::Register(const HINSTANCE hInstance) { } InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { + const auto initParamsReader = InfiniFrame::Native::Interop::InitParamsReader(initParams); + initParamsReader.RequireStartContent(); + m_impl = std::make_unique(); - if (initParams->Size != sizeof(InfiniFrameInitParams)) { - auto msg = std::format( - L"Initial parameters passed are {} bytes, but expected {} bytes.", - initParams->Size, sizeof(InfiniFrameInitParams) - ); - MessageBox(nullptr, msg.c_str(), L"Native Initialization Failed", MB_OK); - exit(0); - } - if (initParams->Title != nullptr) { + if (initParams->Title != nullptr) m_impl->_windowTitle = ToUTF16String(initParams->Title); - if (initParams->NotificationsEnabled) { - WinToast::instance()->setAppName(m_impl->_windowTitle.c_str()); - if (m_impl->_notificationRegistrationId.empty()) - WinToast::instance()->setAppUserModelId(m_impl->_windowTitle.c_str()); - } - } if (initParams->StartUrl != nullptr) m_impl->_startUrl = ToUTF16String(initParams->StartUrl); @@ -239,6 +126,9 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { if (initParams->StartString != nullptr) m_impl->_startString = ToUTF16String(initParams->StartString); + if (m_impl->_startUrl.empty() && m_impl->_startString.empty()) + throw std::invalid_argument("Either StartUrl or StartString must be specified."); + if (initParams->TemporaryFilesPath != nullptr) m_impl->_temporaryFilesPath = ToUTF16String(initParams->TemporaryFilesPath); @@ -265,6 +155,7 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { m_impl->_smoothScrollingEnabled = initParams->SmoothScrollingEnabled; m_impl->_ignoreCertificateErrorsEnabled = initParams->IgnoreCertificateErrorsEnabled; m_impl->_notificationsEnabled = initParams->NotificationsEnabled; + m_impl->ConfigureNotificationIdentityForTitle(m_impl->_windowTitle); m_impl->_zoom = initParams->Zoom; m_impl->_minWidth = initParams->MinWidth; @@ -359,10 +250,7 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { _hInstance, //Instance handle this //Additional application data ); - { - std::lock_guard lock(hwndMapMutex); - hwndToInfiniFrame[m_impl->_hWnd] = this; - } + InfiniFrame::Platform::Windows::TrackWindowInstance(m_impl->_hWnd, this); if (initParams->WindowIconFile != nullptr) { SetIconFile(initParams->WindowIconFile); @@ -383,13 +271,7 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { if (initParams->Topmost) SetTopmost(true); - if (initParams->NotificationsEnabled) { - if (!m_impl->_notificationRegistrationId.empty()) - WinToast::instance()->setAppUserModelId(m_impl->_notificationRegistrationId.c_str()); - - m_impl->_toastHandler = std::make_unique(this); - WinToast::instance()->initialize(); - } + m_impl->InitializeNotifications(this); m_impl->_dialog = std::make_unique(this); @@ -398,6 +280,7 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { } InfiniFrameWindow::~InfiniFrameWindow() { + CloseWebView(); } HWND InfiniFrameWindow::getHwnd() { @@ -405,160 +288,9 @@ HWND InfiniFrameWindow::getHwnd() { } -LRESULT CALLBACK WindowProc(const HWND hwnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) { - switch (uMsg) { - case WM_CREATE: { - EnableDarkMode(hwnd, true); - if (IsDarkModeEnabled()) - RefreshNonClientArea(hwnd); - break; - } - case WM_DPICHANGED: { - RECT* newWindowRect = reinterpret_cast(lParam); - - SetWindowPos( - hwnd, - nullptr, - newWindowRect->left, - newWindowRect->top, - newWindowRect->right - newWindowRect->left, - newWindowRect->bottom - newWindowRect->top, - SWP_NOZORDER | SWP_NOACTIVATE - ); - - return 0; - } - case WM_SETTINGCHANGE: { - if (IsColorSchemeChange(lParam)) - SendMessageW(hwnd, WM_THEMECHANGED, 0, 0); - - break; - } - case WM_THEMECHANGED: { - EnableDarkMode(hwnd, IsDarkModeEnabled()); - RefreshNonClientArea(hwnd); - InvalidateRect(hwnd, nullptr, TRUE); - break; - } - case WM_PAINT: { - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - - // Fill the background with the current theme color - if (IsDarkModeEnabled()) { - FillRect(hdc, &ps.rcPaint, detail::BrushManager::instance().dark()); - } - else { - FillRect(hdc, &ps.rcPaint, detail::BrushManager::instance().light()); - } - - EndPaint(hwnd, &ps); - break; - } - case WM_ACTIVATE: { - InfiniFrameWindow * instance = hwndToInfiniFrame[hwnd]; - if (instance) { - if (LOWORD(wParam) == WA_INACTIVE) { - instance->InvokeFocusOut(); - } - else { - instance->FocusWebView2(); - instance->InvokeFocusIn(); - - return 0; - } - } - break; - } - case WM_CLOSE: { - InfiniFrameWindow * instance = hwndToInfiniFrame[hwnd]; - if (instance) { - bool doNotClose = instance->InvokeClose(); - - if (!doNotClose) { - DestroyWindow(hwnd); - } - } - - return 0; - } - case WM_DESTROY: { - InfiniFrameWindow * instance = hwndToInfiniFrame[hwnd]; - if (instance) { - instance->CloseWebView(); - } - { - std::lock_guard lock(hwndMapMutex); - hwndToInfiniFrame.erase(hwnd); - } - // Terminate the message loop of the thread that owns this window - if (hwnd == messageLoopRootWindowHandle) - PostQuitMessage(0); - - return 0; - } - case WM_USER_INVOKE: { - auto callback = reinterpret_cast(wParam); - callback(); - auto* waitInfo = reinterpret_cast(lParam); - { - std::lock_guard guard(invokeLockMutex); - waitInfo->isCompleted = true; - } - waitInfo->completionNotifier.notify_one(); - return 0; - } - case WM_GETMINMAXINFO: { - InfiniFrameWindow * instance = hwndToInfiniFrame[hwnd]; - if (instance == nullptr) - return 0; - - MINMAXINFO* mmi = reinterpret_cast(lParam); - if (instance->m_impl->_minWidth > 0) - mmi->ptMinTrackSize.x = instance->m_impl->_minWidth; - if (instance->m_impl->_minHeight > 0) - mmi->ptMinTrackSize.y = instance->m_impl->_minHeight; - if (instance->m_impl->_maxWidth < INT_MAX) - mmi->ptMaxTrackSize.x = instance->m_impl->_maxWidth; - if (instance->m_impl->_maxHeight < INT_MAX) - mmi->ptMaxTrackSize.y = instance->m_impl->_maxHeight; - return 0; - } - case WM_SIZE: { - InfiniFrameWindow * instance = hwndToInfiniFrame[hwnd]; - if (instance) { - instance->RefitContent(); - int width, height; - instance->GetSize(&width, &height); - instance->InvokeResize(width, height); - - if (LOWORD(wParam) == SIZE_MAXIMIZED) { - instance->InvokeMaximized(); - } - else if (LOWORD(wParam) == SIZE_RESTORED) { - instance->InvokeRestored(); - } - else if (LOWORD(wParam) == SIZE_MINIMIZED) { - instance->InvokeMinimized(); - } - } - return 0; - } - case WM_MOVE: { - InfiniFrameWindow * instance = hwndToInfiniFrame[hwnd]; - if (instance) { - int x, y; - instance->GetPosition(&x, &y); - instance->InvokeMove(x, y); - } - return 0; - } - } - - return DefWindowProc(hwnd, uMsg, wParam, lParam); -} - void InfiniFrameWindow::CloseWebView() { + m_impl->UnregisterWebViewEventHandlers(); + if (m_impl->_webviewController != nullptr) { m_impl->_webviewController->Close(); m_impl->_webviewController = nullptr; @@ -694,10 +426,6 @@ void InfiniFrameWindow::GetFocused(bool* isFocused) const { *isFocused = GetFocus() == m_impl->_hWnd; } -void InfiniFrameWindow::GetNotificationsEnabled(bool* enabled) const { - *enabled = m_impl->_notificationsEnabled; -} - AutoString InfiniFrameWindow::GetIconFileName() const { return AllocateStringCopy(m_impl->_iconFileName); } @@ -726,10 +454,6 @@ void InfiniFrameWindow::GetResizable(bool* resizable) const { *resizable = (lStyles & WS_THICKFRAME) != 0; } -unsigned int InfiniFrameWindow::GetScreenDpi() const { - return GetDpiForWindow(m_impl->_hWnd); -} - void InfiniFrameWindow::GetSize(int* width, int* height) const { RECT rect = {}; GetWindowRect(m_impl->_hWnd, &rect); @@ -979,11 +703,7 @@ void InfiniFrameWindow::SetTitle(AutoString title) { std::wstring wideTitle = ToUTF16String(title); m_impl->_windowTitle = wideTitle; SetWindowText(m_impl->_hWnd, wideTitle.c_str()); - if (m_impl->_notificationsEnabled) { - WinToast::instance()->setAppName(wideTitle.c_str()); - if (m_impl->_notificationRegistrationId.empty()) - WinToast::instance()->setAppUserModelId(wideTitle.c_str()); - } + m_impl->ConfigureNotificationIdentityForTitle(wideTitle); } void InfiniFrameWindow::SetTopmost(const bool topmost) { @@ -1042,86 +762,18 @@ void InfiniFrameWindow::SetFocused() { FocusWebView2(); } -void InfiniFrameWindow::ShowNotification(AutoString title, AutoString body) { - std::wstring wideTitle = ToUTF16String(title); - std::wstring wideBody = ToUTF16String(body); - if (m_impl->_notificationsEnabled && WinToast::isCompatible()) { - WinToastTemplate toast = WinToastTemplate(WinToastTemplate::ImageAndText02); - toast.setTextField(wideTitle.c_str(), WinToastTemplate::FirstLine); - toast.setTextField(wideBody.c_str(), WinToastTemplate::SecondLine); - if (!m_impl->_iconFileName.empty()) - toast.setImagePath(m_impl->_iconFileName); - WinToast::instance()->showToast(toast, m_impl->_toastHandler.get()); - } -} - void InfiniFrameWindow::WaitForExit() { - messageLoopRootWindowHandle = m_impl->_hWnd; + InfiniFrame::Platform::Windows::MessageLoopRootWindowHandle = m_impl->_hWnd; // Run the message loop MSG msg = {}; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); + m_impl->ThrowIfWebViewInitializationFailed(); } -} - -//Callbacks -BOOL MonitorEnum(const HMONITOR monitor, HDC, LPRECT, const LPARAM arg) { - auto callback = reinterpret_cast(arg); - UINT dpiX, dpiY; - MONITORINFO info = {}; - info.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(monitor, &info); - GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); - Monitor props = {}; - props.monitor.x = info.rcMonitor.left; - props.monitor.y = info.rcMonitor.top; - props.monitor.width = info.rcMonitor.right - info.rcMonitor.left; - props.monitor.height = info.rcMonitor.bottom - info.rcMonitor.top; - props.work.x = info.rcWork.left; - props.work.y = info.rcWork.top; - props.work.width = info.rcWork.right - info.rcWork.left; - props.work.height = info.rcWork.bottom - info.rcWork.top; - props.scale = dpiY / 96.0; - return callback(&props) ? TRUE : FALSE; -} - -void InfiniFrameWindow::GetAllMonitors(GetAllMonitorsCallback callback) const { - if (callback) { - EnumDisplayMonitors( - nullptr, nullptr, reinterpret_cast(MonitorEnum), - reinterpret_cast(callback) - ); - } -} - -void InfiniFrameWindow::Invoke(ACTION callback) { - if (!callback) - return; - - if (m_impl->_hWnd == nullptr || !IsWindow(m_impl->_hWnd)) - return; - - InvokeWaitInfo waitInfo = {}; - if (!PostMessage( - m_impl->_hWnd, WM_USER_INVOKE, reinterpret_cast(callback), reinterpret_cast(&waitInfo) - )) - return; - - std::unique_lock uLock(invokeLockMutex); - const bool completed = waitInfo.completionNotifier.wait_for( - uLock, - std::chrono::seconds(15), - [&] { - return waitInfo.isCompleted; - } - ); - - if (!completed) { - OutputDebugStringW(L"InfiniFrameWindow::Invoke timed out waiting for UI thread callback.\n"); - } + m_impl->ThrowIfWebViewInitializationFailed(); } std::string InfiniFrameWindow::ToUTF8String(const AutoString source) const { @@ -1132,560 +784,6 @@ std::wstring InfiniFrameWindow::ToUTF16String(const AutoString source) const { return Utf8ToWide(source); } -void InfiniFrameWindow::AttachWebView() { - size_t runtimePathLen = wcsnlen(_webview2RuntimePath, _countof(_webview2RuntimePath)); - PCWSTR runtimePath = runtimePathLen > 0 ? &_webview2RuntimePath[0] : nullptr; - - std::wstring startupString; - if (!m_impl->_userAgent.empty()) - startupString += L"--user-agent=\"" + m_impl->_userAgent + L"\" "; - if (m_impl->_mediaAutoplayEnabled) - startupString += L"--autoplay-policy=no-user-gesture-required "; - if (m_impl->_fileSystemAccessEnabled) - startupString += L"--allow-file-access-from-files "; - if (!m_impl->_webSecurityEnabled) - startupString += L"--disable-web-security "; - if (m_impl->_javascriptClipboardAccessEnabled) - startupString += L"--enable-javascript-clipboard-access "; - if (m_impl->_mediaStreamEnabled) - startupString += L"--enable-usermedia-screen-capturing "; - if (!m_impl->_smoothScrollingEnabled) - startupString += L"--disable-smooth-scrolling "; - if (m_impl->_ignoreCertificateErrorsEnabled) - startupString += L"--ignore-certificate-errors "; - if (!m_impl->_browserControlInitParameters.empty()) - startupString += m_impl->_browserControlInitParameters; //e.g.--hide-scrollbars - - auto options = Microsoft::WRL::Make(); - if (startupString.length() > 0) - options->put_AdditionalBrowserArguments(startupString.c_str()); - - bool requiresAppSchemeRegistration = std::any_of( - m_impl->_customSchemeNames.begin(), - m_impl->_customSchemeNames.end(), - [](const std::wstring& schemeName) { - return _wcsicmp(schemeName.c_str(), L"app") == 0; - } - ); - bool appSchemeRegistrationSupported = false; - - // Register custom schemes with WebView2 so top-level navigations like app://... are allowed. - if (!m_impl->_customSchemeNames.empty()) { - wil::com_ptr options4; - if (SUCCEEDED(options->QueryInterface(IID_PPV_ARGS(&options4))) && options4) { - appSchemeRegistrationSupported = true; - std::vector> registrations; - registrations.reserve(m_impl->_customSchemeNames.size()); - - for (const auto& schemeName : m_impl->_customSchemeNames) { - auto registration = Microsoft::WRL::Make(schemeName.c_str()); - if (!registration) - continue; - - // Only the embedded-assets scheme uses app://localhost/... and should be - // treated as secure with an authority component. - if (_wcsicmp(schemeName.c_str(), L"app") == 0) { - registration->put_HasAuthorityComponent(TRUE); - registration->put_TreatAsSecure(TRUE); - } - registrations.emplace_back(registration); - } - - if (!registrations.empty()) { - std::vector rawRegistrations; - rawRegistrations.reserve(registrations.size()); - for (auto& registration : registrations) - rawRegistrations.emplace_back(registration.get()); - - options4->SetCustomSchemeRegistrations( - static_cast(rawRegistrations.size()), - rawRegistrations.data() - ); - } - } - } - - if (requiresAppSchemeRegistration && !appSchemeRegistrationSupported) { - MessageBox( - m_impl->_hWnd, - L"This app requires WebView2 custom scheme registration for app://localhost/. Please update WebView2 Runtime to a version that supports ICoreWebView2EnvironmentOptions4.", - L"WebView2 Runtime Too Old", - MB_OK | MB_ICONERROR - ); - return; - } - - HRESULT envResult = CreateCoreWebView2EnvironmentWithOptions( - runtimePath, - m_impl->_temporaryFilesPath.empty() - ? nullptr - : m_impl->_temporaryFilesPath.c_str(), - options.Get(), - Callback< - ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>( - [&]( - const HRESULT result, - ICoreWebView2Environment* env - ) -> HRESULT { - if (result != S_OK) { - return result; - } - HRESULT envResult = env->QueryInterface( - &m_impl->_webviewEnvironment - ); - if (envResult != S_OK) { - return envResult; - } - - env->CreateCoreWebView2Controller( - m_impl->_hWnd, - Callback< - ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>( - [&]( - const HRESULT result, - ICoreWebView2Controller* controller - ) -> - HRESULT { - if (result != S_OK) { - return result; - } - - HRESULT envResult = controller-> - QueryInterface( - &m_impl-> - _webviewController - ); - if (envResult != S_OK) { - return envResult; - } - m_impl->_webviewController->get_CoreWebView2(&m_impl->_webviewWindow); - - const auto js_wide = Embedded::InfiniFrameHostJsUtf16(); - OutputDebugStringW(std::format(L"[InfiniFrame] Bridge script length: {} chars\n", js_wide.size()).c_str()); - - // AddScriptToExecuteOnDocumentCreated is async: the script is not - // registered in the browser process until the completion callback fires. - // We must not navigate until then, otherwise fast local navigations - // (e.g., app://localhost/) reach ContentLoading before the bridge script - // exists, and Blazor's Boot.WebView.ts throws because - // window.external.receiveMessage is undefined. - // - // If script registration fails for any reason (e.g., empty resource), - // we fall through and navigate anyway so the page still loads. - struct NavigateOnce { - InfiniFrameWindow* self; - bool fired = false; - void navigate() { - if (fired) return; - fired = true; - if (!self->m_impl->_startUrl.empty()) - self->m_impl->_webviewWindow->Navigate(self->m_impl->_startUrl.c_str()); - else if (!self->m_impl->_startString.empty()) - self->m_impl->_webviewWindow->NavigateToString(self->m_impl->_startString.c_str()); - else { - MessageBox(nullptr, - L"Neither StartUrl nor StartString was specified", - L"Native Initialization Failed", MB_OK); - exit(0); - } - } - }; - auto nav = std::make_shared(NavigateOnce{this}); - - HRESULT addScriptHr = m_impl->_webviewWindow->AddScriptToExecuteOnDocumentCreated( - js_wide.c_str(), - Callback( - [nav](HRESULT errorCode, LPCWSTR id) -> HRESULT { - OutputDebugStringW(std::format(L"[InfiniFrame] AddScriptToExecuteOnDocumentCreated callback: hr=0x{:08X} id={}\n", (unsigned)errorCode, id ? id : L"(null)").c_str()); - nav->navigate(); - return S_OK; - } - ).Get() - ); - - // If AddScriptToExecuteOnDocumentCreated itself failed synchronously - // (e.g., empty script string on some WebView2 versions), navigate now - // so the page is not left blank. - if (FAILED(addScriptHr)) - nav->navigate(); - - wil::com_ptr - settings; - HRESULT settingsResult = m_impl-> - _webviewWindow->get_Settings( - &settings - ); - if (FAILED(settingsResult) || ! - settings) { - return FAILED(settingsResult) - ? settingsResult - : E_FAIL; - } - settings-> - put_AreHostObjectsAllowed( - TRUE - ); - settings->put_IsScriptEnabled( - TRUE - ); - settings-> - put_AreDefaultScriptDialogsEnabled( - TRUE - ); - settings->put_IsWebMessageEnabled( - TRUE - ); - - EventRegistrationToken - webMessageToken; - - m_impl->_webviewWindow-> - add_WebMessageReceived( - Callback< - ICoreWebView2WebMessageReceivedEventHandler>( - [&]( - ICoreWebView2*, - ICoreWebView2WebMessageReceivedEventArgs - * args - ) -> HRESULT { - wil::unique_cotaskmem_string - message; - wil::unique_cotaskmem_string - source; - args-> - TryGetWebMessageAsString( - &message - ); - args-> - get_Source( - &source - ); - if ( - (source.get() == nullptr - || source.get()[0] == L'\0') - && m_impl->_webviewWindow != nullptr - ) { - m_impl-> - _webviewWindow-> - get_Source( - &source - ); - } - m_impl-> - _webMessageReceivedCallback( - message. - get(), - source. - get() - ); - return S_OK; - } - ).Get(), - &webMessageToken - ); - - EventRegistrationToken - webResourceRequestedToken; - auto webview23 = m_impl->_webviewWindow.try_query(); - if (webview23) { - webview23->AddWebResourceRequestedFilterWithRequestSourceKinds( - L"*", - COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL, - COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL - ); - } - else { - m_impl->_webviewWindow-> - AddWebResourceRequestedFilter( - L"*", - COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL - ); - } - m_impl->_webviewWindow-> - add_WebResourceRequested( - Callback< - ICoreWebView2WebResourceRequestedEventHandler>( - [&]( - ICoreWebView2*, - ICoreWebView2WebResourceRequestedEventArgs - * args - ) { - wil::com_ptr< - ICoreWebView2WebResourceRequest> - req; - if (FAILED( - args-> - get_Request( - &req - ) - ) - || ! - req) - return S_OK; - - wil::unique_cotaskmem_string - uri; - req->get_Uri(&uri); - std::wstring - uriString = uri - .get(); - wil::com_ptr - requestHeaders; - std::wstring requestOrigin; - if (SUCCEEDED(req->get_Headers(&requestHeaders)) && requestHeaders) { - wil::unique_cotaskmem_string originHeaderValue; - if (SUCCEEDED( - requestHeaders->GetHeader(L"Origin", &originHeaderValue) - ) - && originHeaderValue.get() != nullptr - && originHeaderValue.get()[0] != L'\0') { - requestOrigin = originHeaderValue.get(); - } - } - - if (uriString.find(L"/_framework/blazor.modules.json") != - std::wstring::npos) { - static constexpr BYTE emptyModuleArray[] = {'[', ']'}; - wil::com_ptr dataStream; - dataStream.attach( - SHCreateMemStream(emptyModuleArray, sizeof(emptyModuleArray)) - ); - if (!dataStream) - return S_OK; - - std::wstring responseHeaders = L"Content-Type: application/json"; - responseHeaders += - L"\r\nAccess-Control-Allow-Methods: GET, HEAD, OPTIONS"; - responseHeaders += L"\r\nAccess-Control-Allow-Headers: *"; - if (!requestOrigin.empty()) { - responseHeaders += L"\r\nAccess-Control-Allow-Origin: " + - requestOrigin; - responseHeaders += - L"\r\nAccess-Control-Allow-Credentials: true"; - responseHeaders += L"\r\nVary: Origin"; - } - else { - responseHeaders += L"\r\nAccess-Control-Allow-Origin: *"; - } - - wil::com_ptr response; - m_impl->_webviewEnvironment->CreateWebResourceResponse( - dataStream.get(), - 200, - L"OK", - responseHeaders.c_str(), - &response - ); - args->put_Response(response.get()); - return S_OK; - } - size_t colonPos = - uriString.find( - L':', 0 - ); - if (colonPos > 0) { - std::wstring - scheme = - uriString - .substr( - 0, - colonPos - ); - auto it = - std::find( - m_impl - -> - _customSchemeNames - .begin(), - m_impl - -> - _customSchemeNames - .end(), - scheme - ); - - if (it != - m_impl-> - _customSchemeNames - .end() && - m_impl-> - _customSchemeCallback - != - nullptr) { - int - numBytes; - AutoString - contentType - = nullptr; - wil::unique_cotaskmem - dotNetResponse( - m_impl - -> - _customSchemeCallback( - const_cast - - (uriString - .c_str()), - &numBytes, - &contentType - ) - ); - auto - freeContentType - = wil::scope_exit( - [& - contentType - ] { - CoTaskMemFree( - contentType - ); - } - ); - - if ( - dotNetResponse - != - nullptr - && - contentType - != - nullptr) { - std::wstring - contentTypeWS - = contentType; - - wil::com_ptr - - dataStream; - dataStream - .attach( - SHCreateMemStream( - reinterpret_cast - - (dotNetResponse - .get()), - numBytes - ) - ); - if (! - dataStream) - return - S_OK; - wil::com_ptr - - response; - std::wstring responseHeaders = L"Content-Type: " + - contentTypeWS; - responseHeaders += - L"\r\nAccess-Control-Allow-Methods: GET, HEAD, OPTIONS"; - responseHeaders += L"\r\nAccess-Control-Allow-Headers: *"; - if (!requestOrigin.empty()) { - responseHeaders += L"\r\nAccess-Control-Allow-Origin: " - + requestOrigin; - responseHeaders += - L"\r\nAccess-Control-Allow-Credentials: true"; - responseHeaders += L"\r\nVary: Origin"; - } - else { - responseHeaders += - L"\r\nAccess-Control-Allow-Origin: *"; - } - m_impl - -> - _webviewEnvironment - -> - CreateWebResourceResponse( - dataStream - .get(), - 200, - L"OK", - responseHeaders.c_str(), - &response - ); - args-> - put_Response( - response - .get() - ); - } - } - } - - return S_OK; - } - ).Get(), - &webResourceRequestedToken - ); - - EventRegistrationToken - permissionRequestedToken; - m_impl->_webviewWindow-> - add_PermissionRequested( - Callback< - ICoreWebView2PermissionRequestedEventHandler>( - [&]( - ICoreWebView2*, - ICoreWebView2PermissionRequestedEventArgs - * args - ) -> HRESULT { - if (m_impl-> - _grantBrowserPermissions) - args-> - put_State( - COREWEBVIEW2_PERMISSION_STATE_ALLOW - ); - return S_OK; - } - ) - .Get(), - &permissionRequestedToken - ); - - if (m_impl->_contextMenuEnabled == - false) - SetContextMenuEnabled(false); - - if (m_impl->_zoomEnabled == false) - SetZoomEnabled(false); - - if (m_impl->_devToolsEnabled == - false) - SetDevToolsEnabled(false); - - if (m_impl->_transparentEnabled == - true) - SetTransparentEnabled(true); - - if (m_impl->_zoom != 100) - SetZoom(m_impl->_zoom); - - RefitContent(); - - FocusWebView2(); - - // Re-apply if topmost was requested - if (m_impl->_topmost) - SetTopmost(true); - - return S_OK; - } - ).Get() - ); - return S_OK; - } - ).Get() - ); - - if (envResult != S_OK) { - _com_error err(envResult); - LPCTSTR errMsg = err.ErrorMessage(); - MessageBox(m_impl->_hWnd, errMsg, L"Error instantiating webview", MB_OK); - } -} - - bool InfiniFrameWindow::EnsureWebViewIsInstalled() { LPWSTR versionInfo = nullptr; HRESULT ensureInstalledResult = GetAvailableCoreWebView2BrowserVersionString(nullptr, &versionInfo); @@ -1808,10 +906,13 @@ void InfiniFrameWindow::Show(const bool isAlreadyShown) { // WebView2 must be created after the window is visible. if (!m_impl->_webviewController) { - if (wcsnlen(_webview2RuntimePath, _countof(_webview2RuntimePath)) > 0 || EnsureWebViewIsInstalled()) - AttachWebView(); - else - exit(0); + if (wcsnlen(_webview2RuntimePath, _countof(_webview2RuntimePath)) == 0 && !EnsureWebViewIsInstalled()) { + DestroyWindow(m_impl->_hWnd); + m_impl->_hWnd = nullptr; + throw std::runtime_error("WebView2 Runtime is not installed and automatic installation failed."); + } + + AttachWebView(); } } @@ -1907,4 +1008,4 @@ void InfiniFrameWindow::InvokeRestored() const noexcept { void InfiniFrameWindow::InvokeMinimized() const noexcept { if (m_impl->_minimizedCallback) m_impl->_minimizedCallback(); -} \ No newline at end of file +} diff --git a/src/InfiniFrame.Native/Platform/Windows/WindowImpl.Win32.h b/src/InfiniFrame.Native/Platform/Windows/WindowImpl.Win32.h new file mode 100644 index 000000000..a8c0ae1b6 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WindowImpl.Win32.h @@ -0,0 +1,88 @@ +#pragma once +/** + * @file WindowImpl.Win32.h + * @brief Private Win32/WebView2 implementation state for InfiniFrameWindow. + */ + +#ifndef INFINIFRAME_PLATFORM_WINDOWS_WINDOWIMPL_WIN32_H +#define INFINIFRAME_PLATFORM_WINDOWS_WINDOWIMPL_WIN32_H + +#include +#include + +#include +#include +#include + +#include "Core/InfiniFrameWindow.h" +#include "Core/InfiniFrameWindowImpl.h" +#include "ToastHandler.h" +#include "Utils/Common.h" + +struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { + std::wstring _temporaryFilesPath; + std::wstring _notificationRegistrationId; + + bool _notificationsEnabled = false; + bool _isInitialized = false; + bool _isWebView2Initializing = false; + bool _centerOnInitialize = false; + bool _chromeless = false; + bool _fullScreen = false; + bool _maximized = false; + bool _minimized = false; + bool _resizable = true; + bool _topmost = false; + bool _useOsDefaultLocation = false; + bool _useOsDefaultSize = false; + bool _hasSavedRect = false; + + RECT _savedRect = {}; + + int _zoom = 100; + int _minWidth = MinWindowDimension; + int _minHeight = MinWindowDimension; + int _maxWidth = MaxWindowDimension; + int _maxHeight = MaxWindowDimension; + + HWND _hWnd = nullptr; + wil::com_ptr _webviewController; + wil::com_ptr _webviewWindow; + wil::com_ptr _webviewEnvironment; + + EventRegistrationToken _webMessageReceivedToken = {}; + EventRegistrationToken _webResourceRequestedTokenForCustomScheme = {}; + EventRegistrationToken _permissionRequestedToken = {}; + + bool _webMessageReceivedRegistered = false; + bool _webResourceRequestedRegistered = false; + bool _permissionRequestedRegistered = false; + bool _webviewInitializationFailed = false; + + HRESULT _webviewInitializationResult = S_OK; + std::wstring _webviewInitializationError; + + std::unique_ptr _toastHandler; + + void FailWebViewInitialization(HRESULT result, const wchar_t* stage) noexcept; + void MarkWebViewInitialized() noexcept; + void ThrowIfWebViewInitializationFailed() const; + void WaitForWebViewInitialization(); + void UnregisterWebViewEventHandlers() noexcept; + void ConfigureNotificationIdentityForTitle(const std::wstring& title); + void InitializeNotifications(InfiniFrameWindow* window); + + HRESULT ConfigureWebViewSettings() const; + void RegisterPermissionRequestedHandler(); + + void RegisterBridgeScriptAndNavigate(); + void NavigateToInitialContent(); + + void RegisterWebMessageReceivedHandler(); + HRESULT HandleWebMessageReceived(ICoreWebView2WebMessageReceivedEventArgs* args); + + void RegisterWebResourceRequestedHandler(); + HRESULT HandleWebResourceRequested(ICoreWebView2WebResourceRequestedEventArgs* args); +}; + +#endif // INFINIFRAME_PLATFORM_WINDOWS_WINDOWIMPL_WIN32_H diff --git a/src/InfiniFrame.Native/Platform/Windows/WindowProc.Win32.cpp b/src/InfiniFrame.Native/Platform/Windows/WindowProc.Win32.cpp new file mode 100644 index 000000000..fcfba9e74 --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WindowProc.Win32.cpp @@ -0,0 +1,230 @@ +#include "WindowProc.Win32.h" + +#include +#include +#include + +#include "DarkMode.h" +#include "WindowImpl.Win32.h" + +namespace InfiniFrame::Platform::Windows { + std::mutex InvokeLockMutex; + thread_local HWND MessageLoopRootWindowHandle = nullptr; + + namespace { + std::mutex HwndMapMutex; + std::map HwndToInfiniFrame; + + class BrushManager { + public: + static BrushManager& instance() noexcept { + static BrushManager inst; + return inst; + } + + HBRUSH dark() const noexcept { + return static_cast(m_darkBrush.get()); + } + + HBRUSH light() const noexcept { + return static_cast(m_lightBrush.get()); + } + + private: + BrushManager() noexcept { + m_darkBrush.reset(CreateSolidBrush(RGB(0, 0, 0))); + m_lightBrush.reset(CreateSolidBrush(RGB(255, 255, 255))); + } + + ~BrushManager() noexcept = default; + + struct HBRUSHDeleter { + void operator()(void* h) const noexcept { + if (h) + DeleteObject(static_cast(h)); + } + }; + + std::unique_ptr m_darkBrush; + std::unique_ptr m_lightBrush; + }; + + InfiniFrameWindow* TryGetWindowInstance(HWND hwnd) { + std::lock_guard lock(HwndMapMutex); + const auto it = HwndToInfiniFrame.find(hwnd); + return it == HwndToInfiniFrame.end() ? nullptr : it->second; + } + + void UntrackWindowInstance(HWND hwnd) { + std::lock_guard lock(HwndMapMutex); + HwndToInfiniFrame.erase(hwnd); + } + } + + HBRUSH DarkBackgroundBrush() noexcept { + return BrushManager::instance().dark(); + } + + HBRUSH LightBackgroundBrush() noexcept { + return BrushManager::instance().light(); + } + + void TrackWindowInstance(HWND hwnd, InfiniFrameWindow* instance) { + if (hwnd == nullptr || instance == nullptr) + return; + + std::lock_guard lock(HwndMapMutex); + HwndToInfiniFrame[hwnd] = instance; + } +} + +LRESULT CALLBACK WindowProc(const HWND hwnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) { + using namespace InfiniFrame::Platform::Windows; + + switch (uMsg) { + case WM_CREATE: { + EnableDarkMode(hwnd, true); + if (IsDarkModeEnabled()) + RefreshNonClientArea(hwnd); + break; + } + case WM_DPICHANGED: { + RECT* newWindowRect = reinterpret_cast(lParam); + + SetWindowPos( + hwnd, + nullptr, + newWindowRect->left, + newWindowRect->top, + newWindowRect->right - newWindowRect->left, + newWindowRect->bottom - newWindowRect->top, + SWP_NOZORDER | SWP_NOACTIVATE + ); + + return 0; + } + case WM_SETTINGCHANGE: { + if (IsColorSchemeChange(lParam)) + SendMessageW(hwnd, WM_THEMECHANGED, 0, 0); + + break; + } + case WM_THEMECHANGED: { + EnableDarkMode(hwnd, IsDarkModeEnabled()); + RefreshNonClientArea(hwnd); + InvalidateRect(hwnd, nullptr, TRUE); + break; + } + case WM_PAINT: { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + + if (IsDarkModeEnabled()) { + FillRect(hdc, &ps.rcPaint, DarkBackgroundBrush()); + } + else { + FillRect(hdc, &ps.rcPaint, LightBackgroundBrush()); + } + + EndPaint(hwnd, &ps); + break; + } + case WM_ACTIVATE: { + InfiniFrameWindow* instance = TryGetWindowInstance(hwnd); + if (instance) { + if (LOWORD(wParam) == WA_INACTIVE) { + instance->InvokeFocusOut(); + } + else { + instance->FocusWebView2(); + instance->InvokeFocusIn(); + + return 0; + } + } + break; + } + case WM_CLOSE: { + InfiniFrameWindow* instance = TryGetWindowInstance(hwnd); + if (instance) { + bool doNotClose = instance->InvokeClose(); + + if (!doNotClose) { + DestroyWindow(hwnd); + } + } + + return 0; + } + case WM_DESTROY: { + InfiniFrameWindow* instance = TryGetWindowInstance(hwnd); + if (instance) { + instance->CloseWebView(); + } + UntrackWindowInstance(hwnd); + + if (hwnd == MessageLoopRootWindowHandle) + PostQuitMessage(0); + + return 0; + } + case InvokeMessage: { + auto callback = reinterpret_cast(wParam); + callback(); + auto* waitInfo = reinterpret_cast(lParam); + { + std::lock_guard guard(InvokeLockMutex); + waitInfo->isCompleted = true; + } + waitInfo->completionNotifier.notify_one(); + return 0; + } + case WM_GETMINMAXINFO: { + InfiniFrameWindow* instance = TryGetWindowInstance(hwnd); + if (instance == nullptr) + return 0; + + MINMAXINFO* mmi = reinterpret_cast(lParam); + if (instance->m_impl->_minWidth > 0) + mmi->ptMinTrackSize.x = instance->m_impl->_minWidth; + if (instance->m_impl->_minHeight > 0) + mmi->ptMinTrackSize.y = instance->m_impl->_minHeight; + if (instance->m_impl->_maxWidth < INT_MAX) + mmi->ptMaxTrackSize.x = instance->m_impl->_maxWidth; + if (instance->m_impl->_maxHeight < INT_MAX) + mmi->ptMaxTrackSize.y = instance->m_impl->_maxHeight; + return 0; + } + case WM_SIZE: { + InfiniFrameWindow* instance = TryGetWindowInstance(hwnd); + if (instance) { + instance->RefitContent(); + int width, height; + instance->GetSize(&width, &height); + instance->InvokeResize(width, height); + + if (LOWORD(wParam) == SIZE_MAXIMIZED) { + instance->InvokeMaximized(); + } + else if (LOWORD(wParam) == SIZE_RESTORED) { + instance->InvokeRestored(); + } + else if (LOWORD(wParam) == SIZE_MINIMIZED) { + instance->InvokeMinimized(); + } + } + return 0; + } + case WM_MOVE: { + InfiniFrameWindow* instance = TryGetWindowInstance(hwnd); + if (instance) { + int x, y; + instance->GetPosition(&x, &y); + instance->InvokeMove(x, y); + } + return 0; + } + } + + return DefWindowProc(hwnd, uMsg, wParam, lParam); +} diff --git a/src/InfiniFrame.Native/Platform/Windows/WindowProc.Win32.h b/src/InfiniFrame.Native/Platform/Windows/WindowProc.Win32.h new file mode 100644 index 000000000..00226effb --- /dev/null +++ b/src/InfiniFrame.Native/Platform/Windows/WindowProc.Win32.h @@ -0,0 +1,38 @@ +#pragma once +/** + * @file WindowProc.Win32.h + * @brief Private Win32 message dispatch helpers for InfiniFrameWindow. + */ + +#ifndef INFINIFRAME_PLATFORM_WINDOWS_WINDOWPROC_WIN32_H +#define INFINIFRAME_PLATFORM_WINDOWS_WINDOWPROC_WIN32_H + +#include +#include + +#include + +#include "Types/Callbacks.h" + +class InfiniFrameWindow; + +namespace InfiniFrame::Platform::Windows { + inline constexpr UINT InvokeMessage = WM_USER + 0x0002; + + struct InvokeWaitInfo { + ACTION callback; + std::condition_variable completionNotifier; + bool isCompleted; + }; + + extern std::mutex InvokeLockMutex; + extern thread_local HWND MessageLoopRootWindowHandle; + + HBRUSH DarkBackgroundBrush() noexcept; + HBRUSH LightBackgroundBrush() noexcept; + void TrackWindowInstance(HWND hwnd, InfiniFrameWindow* instance); +} + +LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + +#endif // INFINIFRAME_PLATFORM_WINDOWS_WINDOWPROC_WIN32_H diff --git a/src/InfiniFrame.Native/Shared/CustomSchemeResponse.h b/src/InfiniFrame.Native/Shared/CustomSchemeResponse.h new file mode 100644 index 000000000..3c32f1824 --- /dev/null +++ b/src/InfiniFrame.Native/Shared/CustomSchemeResponse.h @@ -0,0 +1,94 @@ +#pragma once +/** + * @file CustomSchemeResponse.h + * @brief Shared helpers for custom-scheme callback responses. + */ + +#ifndef INFINIFRAME_SHARED_CUSTOMSCHEMERESPONSE_H +#define INFINIFRAME_SHARED_CUSTOMSCHEMERESPONSE_H + +#include "Core/InfiniFrame.h" +#include "Interop/NativeBuffer.h" + +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace InfiniFrame::Native::Shared { +#ifdef _WIN32 + inline constexpr AutoStringConst DefaultCustomSchemeContentType = L"application/octet-stream"; + inline constexpr AutoStringConst JsonCustomSchemeContentType = L"application/json"; +#else + inline constexpr AutoStringConst DefaultCustomSchemeContentType = "application/octet-stream"; + inline constexpr AutoStringConst JsonCustomSchemeContentType = "application/json"; +#endif + + struct CustomSchemeResponse { + InfiniFrame::Native::Interop::NativeBufferPtr body; + InfiniFrame::Native::Interop::NativeBufferPtr contentTypeBuffer; + int length = 0; + AutoString contentType = nullptr; + + [[nodiscard]] bool HasBody() const noexcept { + return body != nullptr && length >= 0; + } + + [[nodiscard]] AutoStringConst ContentTypeOrDefault() const noexcept { + return contentType == nullptr ? DefaultCustomSchemeContentType : contentType; + } + }; + + inline CustomSchemeResponse InvokeCustomSchemeCallback( + const WebResourceRequestedCallback callback, + const AutoString url + ) noexcept { + if (callback == nullptr) + return {}; + + int numBytes = 0; + AutoString contentType = nullptr; + auto body = InfiniFrame::Native::Interop::AdoptNativeBuffer( + callback(url, &numBytes, &contentType) + ); + auto contentTypeBuffer = InfiniFrame::Native::Interop::AdoptNativeBuffer(contentType); + + return CustomSchemeResponse{ + std::move(body), + std::move(contentTypeBuffer), + numBytes, + contentType + }; + } + +#ifdef _WIN32 + inline std::wstring BuildCorsResponseHeaders( + const std::wstring_view contentType, + const std::wstring_view requestOrigin + ) { + std::wstring responseHeaders = L"Content-Type: "; + if (contentType.empty()) + responseHeaders += DefaultCustomSchemeContentType; + else + responseHeaders.append(contentType); + responseHeaders += L"\r\nAccess-Control-Allow-Methods: GET, HEAD, OPTIONS"; + responseHeaders += L"\r\nAccess-Control-Allow-Headers: *"; + + if (!requestOrigin.empty()) { + responseHeaders += L"\r\nAccess-Control-Allow-Origin: "; + responseHeaders.append(requestOrigin); + responseHeaders += L"\r\nAccess-Control-Allow-Credentials: true"; + responseHeaders += L"\r\nVary: Origin"; + } + else { + responseHeaders += L"\r\nAccess-Control-Allow-Origin: *"; + } + + return responseHeaders; + } +#endif +} + +#endif // INFINIFRAME_SHARED_CUSTOMSCHEMERESPONSE_H diff --git a/src/InfiniFrame.Native/Utils/Common.h b/src/InfiniFrame.Native/Utils/Common.h index f01864e51..9a9917319 100644 --- a/src/InfiniFrame.Native/Utils/Common.h +++ b/src/InfiniFrame.Native/Utils/Common.h @@ -7,6 +7,8 @@ #ifndef INFINIFRAME_COMMON_H #define INFINIFRAME_COMMON_H +#include "../Interop/NativeString.h" + #include #include #include @@ -155,31 +157,22 @@ template #ifdef _WIN32 inline wchar_t* AllocateStringCopy(const std::wstring& str) { - const size_t len = str.length(); - wchar_t* copy = new wchar_t[len + 1]; - std::memcpy(copy, str.c_str(), (len + 1) * sizeof(wchar_t)); - return copy; + return InfiniFrame::Native::Interop::AllocateNativeStringCopy(str); } #elif __linux__ inline char* AllocateStringCopy(const std::string& str) { - return g_strdup(str.c_str()); + return InfiniFrame::Native::Interop::AllocateNativeStringCopy(str); } #elif __APPLE__ inline char* AllocateStringCopy(const std::string& str) { - const size_t len = str.length(); - char* copy = static_cast(malloc(len + 1)); - std::memcpy(copy, str.c_str(), len + 1); - return copy; + return InfiniFrame::Native::Interop::AllocateNativeStringCopy(str); } #else inline char* AllocateStringCopy(const std::string& str) { - const size_t len = str.length(); - char* copy = static_cast(malloc(len + 1)); - std::memcpy(copy, str.c_str(), len + 1); - return copy; + return InfiniFrame::Native::Interop::AllocateNativeStringCopy(str); } #endif diff --git a/src/InfiniFrame.Native/cmake/Platform.MacOS.cmake b/src/InfiniFrame.Native/cmake/Platform.MacOS.cmake index 8be141301..b68189c85 100644 --- a/src/InfiniFrame.Native/cmake/Platform.MacOS.cmake +++ b/src/InfiniFrame.Native/cmake/Platform.MacOS.cmake @@ -1,15 +1,36 @@ # Configure the macOS native target. # Params: # - target_name: final CMake target name (usually `${PROJECT_NAME}`) +# - common_sources: list of cross-platform source files +# - test_sources: list of test/helper source files compiled into the native target # - mac_sources: list of macOS-only source files # - header_files: list of header files for IDE organization -function(infiniframe_configure_macos_target target_name mac_sources header_files) - configure_file(Exports.cpp ${CMAKE_CURRENT_BINARY_DIR}/Exports.mm COPYONLY) - configure_file(Exports.Tests.cpp ${CMAKE_CURRENT_BINARY_DIR}/Exports.Tests.mm COPYONLY) +function(infiniframe_configure_macos_target target_name common_sources test_sources mac_sources header_files) + set(_mac_common_sources) + + foreach (_source IN LISTS common_sources) + get_filename_component(_source_extension "${_source}" EXT) + + if (_source_extension STREQUAL ".cpp") + get_filename_component(_source_name "${_source}" NAME_WE) + set(_copied_source "${CMAKE_CURRENT_BINARY_DIR}/${_source_name}.mm") + configure_file("${_source}" "${_copied_source}" COPYONLY) + list(APPEND _mac_common_sources "${_copied_source}") + else () + list(APPEND _mac_common_sources "${_source}") + endif () + endforeach () + + set(_mac_test_sources) + + if (test_sources) + configure_file(Exports.Tests.cpp ${CMAKE_CURRENT_BINARY_DIR}/Exports.Tests.mm COPYONLY) + list(APPEND _mac_test_sources ${CMAKE_CURRENT_BINARY_DIR}/Exports.Tests.mm) + endif () add_library(${target_name} SHARED - ${CMAKE_CURRENT_BINARY_DIR}/Exports.mm - ${CMAKE_CURRENT_BINARY_DIR}/Exports.Tests.mm + ${_mac_common_sources} + ${_mac_test_sources} ${mac_sources} ${header_files} ) diff --git a/src/InfiniFrame.Shared/FluentApi/InfiniWindowExtensions.cs b/src/InfiniFrame.Shared/FluentApi/InfiniWindowExtensions.cs index 1183bbfd7..71132782d 100644 --- a/src/InfiniFrame.Shared/FluentApi/InfiniWindowExtensions.cs +++ b/src/InfiniFrame.Shared/FluentApi/InfiniWindowExtensions.cs @@ -27,7 +27,7 @@ public static class InfiniWindowExtensions { /// InfiniFrame window instance public static T Load(this T window, Uri uri) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".Load({uri})", uri); - window.Invoke(() => InfiniFrameNative.NavigateToUrl(window.InstanceHandle, uri.ToString())); + window.Invoke(() => EnsureNative(InfiniFrameNative.NavigateToUrl(window.InstanceHandle, uri.ToString()))); return window; } @@ -82,7 +82,7 @@ public static T Load(this T window, string path) where T : class, IInfiniFram public static T LoadRawString(this T window, string content) where T : class, IInfiniFrameWindow { string shortContent = content.Length > 50 ? string.Concat(content.AsSpan(0, 47), "...") : content; window.Logger.LogDebug(".LoadRawString({Content})", shortContent); - window.Invoke(() => InfiniFrameNative.NavigateToString(window.InstanceHandle, content)); + window.Invoke(() => EnsureNative(InfiniFrameNative.NavigateToString(window.InstanceHandle, content))); return window; } @@ -95,7 +95,7 @@ public static T LoadRawString(this T window, string content) where T : class, /// public static T Center(this T window) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".Center()"); - window.Invoke(() => InfiniFrameNative.Center(window.InstanceHandle)); + window.Invoke(() => EnsureNative(InfiniFrameNative.Center(window.InstanceHandle))); return window; } @@ -109,7 +109,7 @@ public static T Center(this T window) where T : class, IInfiniFrameWindow { public static T CenterOnCurrentMonitor(this T window) where T : class, IInfiniFrameWindow { window.Invoke(() => { ImmutableArray monitors = MonitorsUtility.GetMonitors(window); - InfiniFrameNative.GetWindowRectangle(window.InstanceHandle, out Rectangle rectangle); + EnsureNative(InfiniFrameNative.GetWindowRectangle(window.InstanceHandle, out Rectangle rectangle)); // TODO think about proper unhappy flow here if (!MonitorsUtility.TryGetCurrentMonitor(monitors, rectangle, out InfiniMonitor monitor)) return; @@ -117,7 +117,7 @@ public static T CenterOnCurrentMonitor(this T window) where T : class, IInfin Rectangle area = monitor.MonitorArea; var newLocation = new Point(area.X + area.Width / 2 - rectangle.Width / 2, area.Y + area.Height / 2 - rectangle.Height / 2); - InfiniFrameNative.SetPosition(window.InstanceHandle, newLocation.X, newLocation.Y); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, newLocation.X, newLocation.Y)); }); return window; @@ -142,11 +142,11 @@ public static T CenterOnMonitor(this T window, int monitorIndex) where T : cl return; } - InfiniFrameNative.GetSize(window.InstanceHandle, out Size size); + EnsureNative(InfiniFrameNative.GetSize(window.InstanceHandle, out Size size)); Rectangle area = monitors[monitorIndex].MonitorArea; var newLocation = new Point(area.X + area.Width / 2 - size.Width / 2, area.Y + area.Height / 2 - size.Height / 2); - InfiniFrameNative.SetPosition(window.InstanceHandle, newLocation.X, newLocation.Y); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, newLocation.X, newLocation.Y)); }); return window; @@ -201,7 +201,7 @@ public static T MoveWithinCurrentMonitorArea(this T window, int left, int top : top; } - InfiniFrameNative.SetPosition(window.InstanceHandle, left, top); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, left, top)); }); return window; } @@ -245,8 +245,8 @@ public static T MoveWithinCurrentMonitorArea(this T window, double left, doub public static T Offset(this T window, int left, int top) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".Offset({left}, {top})", left, top); window.Invoke(() => { - InfiniFrameNative.GetPosition(window.InstanceHandle, out int oldLeft, out int oldTop); - InfiniFrameNative.SetPosition(window.InstanceHandle, oldLeft + left, oldTop + top); + EnsureNative(InfiniFrameNative.GetPosition(window.InstanceHandle, out int oldLeft, out int oldTop)); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, oldLeft + left, oldTop + top)); }); return window; } @@ -293,7 +293,7 @@ public static T SetTransparent(this T window, bool enabled) where T : class, } window.Logger.LogDebug("Invoking InfiniFrameNative.SetTransparentEnabled({value})", enabled); - window.Invoke(() => InfiniFrameNative.SetTransparentEnabled(window.InstanceHandle, enabled)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetTransparentEnabled(window.InstanceHandle, enabled))); return window; } @@ -310,10 +310,10 @@ public static T SetContextMenuEnabled(this T window, bool enabled) where T : window.Logger.LogDebug(".SetContextMenuEnabled({Enabled})", enabled); window.Invoke(() => { - InfiniFrameNative.GetContextMenuEnabled(window.InstanceHandle, out bool isEnabled); + EnsureNative(InfiniFrameNative.GetContextMenuEnabled(window.InstanceHandle, out bool isEnabled)); if (isEnabled == enabled) return; - InfiniFrameNative.SetContextMenuEnabled(window.InstanceHandle, enabled); + EnsureNative(InfiniFrameNative.SetContextMenuEnabled(window.InstanceHandle, enabled)); }); return window; @@ -332,10 +332,10 @@ public static T SetDevToolsEnabled(this T window, bool enabled) where T : cla window.Logger.LogDebug(".SetDevTools({Enabled})", enabled); window.Invoke(() => { - InfiniFrameNative.GetDevToolsEnabled(window.InstanceHandle, out bool isEnabled); + EnsureNative(InfiniFrameNative.GetDevToolsEnabled(window.InstanceHandle, out bool isEnabled)); if (isEnabled == enabled) return; - InfiniFrameNative.SetDevToolsEnabled(window.InstanceHandle, enabled); + EnsureNative(InfiniFrameNative.SetDevToolsEnabled(window.InstanceHandle, enabled)); }); return window; @@ -361,21 +361,21 @@ public static T SetFullScreen(this T window, bool fullScreen) where T : class window.Invoke(() => { ImmutableArray monitors = MonitorsUtility.GetMonitors(window); - InfiniFrameNative.GetPosition(window.InstanceHandle, out int left, out int top); - InfiniFrameNative.GetSize(window.InstanceHandle, out int width, out int height); + EnsureNative(InfiniFrameNative.GetPosition(window.InstanceHandle, out int left, out int top)); + EnsureNative(InfiniFrameNative.GetSize(window.InstanceHandle, out int width, out int height)); window.CachedPreFullScreenBounds = new Rectangle(left, top, width, height); if (!MonitorsUtility.TryGetCurrentMonitor(monitors, window.CachedPreFullScreenBounds, out InfiniMonitor currentMonitor)) { window.Logger.LogError("Failed to get current monitor, defaulting to simple fullscreen call"); - InfiniFrameNative.SetFullScreen(window.InstanceHandle, true); + EnsureNative(InfiniFrameNative.SetFullScreen(window.InstanceHandle, true)); return; } Rectangle currentMonitorArea = currentMonitor.MonitorArea; - InfiniFrameNative.SetFullScreen(window.InstanceHandle, true); - InfiniFrameNative.SetPosition(window.InstanceHandle, currentMonitorArea.X, currentMonitorArea.Y); - InfiniFrameNative.SetSize(window.InstanceHandle, currentMonitorArea.Width, currentMonitorArea.Height); + EnsureNative(InfiniFrameNative.SetFullScreen(window.InstanceHandle, true)); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, currentMonitorArea.X, currentMonitorArea.Y)); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, currentMonitorArea.Width, currentMonitorArea.Height)); }); return window; @@ -383,9 +383,9 @@ public static T SetFullScreen(this T window, bool fullScreen) where T : class // Set Fullscreen to false => Restore to previous state window.Invoke(() => { - InfiniFrameNative.SetFullScreen(window.InstanceHandle, false); - InfiniFrameNative.SetPosition(window.InstanceHandle, window.CachedPreFullScreenBounds.X, window.CachedPreFullScreenBounds.Y); - InfiniFrameNative.SetSize(window.InstanceHandle, window.CachedPreFullScreenBounds.Width, window.CachedPreFullScreenBounds.Height); + EnsureNative(InfiniFrameNative.SetFullScreen(window.InstanceHandle, false)); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, window.CachedPreFullScreenBounds.X, window.CachedPreFullScreenBounds.Y)); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, window.CachedPreFullScreenBounds.Width, window.CachedPreFullScreenBounds.Height)); }); return window; @@ -404,8 +404,8 @@ public static T SetHeight(this T window, int height) where T : class, IInfini window.Logger.LogDebug(".SetHeight({Height})", height); window.Invoke(() => { - InfiniFrameNative.GetSize(window.InstanceHandle, out int width, out _); - InfiniFrameNative.SetSize(window.InstanceHandle, width, height); + EnsureNative(InfiniFrameNative.GetSize(window.InstanceHandle, out int width, out _)); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, width, height)); }); return window; @@ -437,7 +437,7 @@ public static T SetIconFile(this T window, string iconFilePath) where T : cla return window; } - window.Invoke(() => InfiniFrameNative.SetIconFile(window.InstanceHandle, resolvedIconFilePath)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetIconFile(window.InstanceHandle, resolvedIconFilePath))); return window; } @@ -454,10 +454,10 @@ public static T SetLeft(this T window, int left) where T : class, IInfiniFram window.Logger.LogDebug(".SetLeft({Left})", left); window.Invoke(() => { - InfiniFrameNative.GetPosition(window.InstanceHandle, out int oldLeft, out int top); + EnsureNative(InfiniFrameNative.GetPosition(window.InstanceHandle, out int oldLeft, out int top)); if (left == oldLeft) return; - InfiniFrameNative.SetPosition(window.InstanceHandle, left, top); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, left, top)); }); return window; @@ -474,7 +474,7 @@ public static T SetLeft(this T window, int left) where T : class, IInfiniFram /// InfiniFrame window instance public static T SetResizable(this T window, bool resizable) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetResizable({Resizable})", resizable); - window.Invoke(() => InfiniFrameNative.SetResizable(window.InstanceHandle, resizable)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetResizable(window.InstanceHandle, resizable))); return window; } @@ -492,7 +492,7 @@ public static T SetResizable(this T window, bool resizable) where T : class, public static T SetSize(this T window, int width, int height) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetSize({Width}, {Height})", width, height); - window.Invoke(() => InfiniFrameNative.SetSize(window.InstanceHandle, width, height)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, width, height))); return window; } @@ -521,10 +521,10 @@ public static T SetSize(this T window, Size size) where T : class, IInfiniFra public static T SetLocation(this T window, int left, int top) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetLocation({left}, {right})", left, top); window.Invoke(() => { - InfiniFrameNative.GetPosition(window.InstanceHandle, out int oldLeft, out int oldTop); + EnsureNative(InfiniFrameNative.GetPosition(window.InstanceHandle, out int oldLeft, out int oldTop)); if (oldLeft == left && oldTop == top) return; - InfiniFrameNative.SetPosition(window.InstanceHandle, left, top); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, left, top)); }); return window; @@ -557,7 +557,7 @@ public static T SetMaximized(this T window, bool maximized) where T : class, window.Logger.LogDebug(".SetMaximized({Maximized})", maximized); window.Invoke(() => { if (!window.Chromeless) { - InfiniFrameNative.SetMaximized(window.InstanceHandle, maximized); + EnsureNative(InfiniFrameNative.SetMaximized(window.InstanceHandle, maximized)); return; } @@ -569,15 +569,15 @@ public static T SetMaximized(this T window, bool maximized) where T : class, Rectangle workArea = monitor.WorkArea; if (maximized) { window.CachedPreMaximizedBounds = windowRect; - InfiniFrameNative.SetPosition(window.InstanceHandle, workArea.Left, workArea.Top); - InfiniFrameNative.SetSize(window.InstanceHandle, workArea.Width, workArea.Height); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, workArea.Left, workArea.Top)); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, workArea.Width, workArea.Height)); window.Events.OnMaximized(); } else if (window.CachedPreMaximizedBounds != Rectangle.Empty) { Rectangle oldRect = window.CachedPreMaximizedBounds; - InfiniFrameNative.SetPosition(window.InstanceHandle, oldRect.Left, oldRect.Top); - InfiniFrameNative.SetSize(window.InstanceHandle, oldRect.Width, oldRect.Height); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, oldRect.Left, oldRect.Top)); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, oldRect.Width, oldRect.Height)); window.CachedPreMaximizedBounds = Rectangle.Empty; window.Events.OnRestored(); } @@ -600,9 +600,9 @@ public static T SetMaximized(this T window, bool maximized) where T : class, public static T ToggleMaximized(this T window) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".ToggleMaximized()"); window.Invoke(() => { - InfiniFrameNative.GetMaximized(window.InstanceHandle, out bool maximized); + EnsureNative(InfiniFrameNative.GetMaximized(window.InstanceHandle, out bool maximized)); if (!window.Chromeless) { - InfiniFrameNative.SetMaximized(window.InstanceHandle, !maximized); + EnsureNative(InfiniFrameNative.SetMaximized(window.InstanceHandle, !maximized)); return; } @@ -616,14 +616,14 @@ public static T ToggleMaximized(this T window) where T : class, IInfiniFrameW Rectangle workArea = monitor.WorkArea; if (window.CachedPreMaximizedBounds == Rectangle.Empty) { window.CachedPreMaximizedBounds = windowRect; - InfiniFrameNative.SetPosition(window.InstanceHandle, workArea.Left, workArea.Top); - InfiniFrameNative.SetSize(window.InstanceHandle, workArea.Width, workArea.Height); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, workArea.Left, workArea.Top)); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, workArea.Width, workArea.Height)); window.Events.OnMaximized(); } else { Rectangle oldRect = window.CachedPreMaximizedBounds; - InfiniFrameNative.SetPosition(window.InstanceHandle, oldRect.Left, oldRect.Top); - InfiniFrameNative.SetSize(window.InstanceHandle, oldRect.Width, oldRect.Height); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, oldRect.Left, oldRect.Top)); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, oldRect.Width, oldRect.Height)); window.CachedPreMaximizedBounds = Rectangle.Empty; window.Events.OnRestored(); } @@ -642,7 +642,7 @@ public static T ToggleMaximized(this T window) where T : class, IInfiniFrameW /// public static T SetMaxSize(this T window, int maxWidth, int maxHeight) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetMaxSize({MaxWidth}, {MaxHeight})", maxWidth, maxHeight); - window.Invoke(() => InfiniFrameNative.SetMaxSize(window.InstanceHandle, maxWidth, maxHeight)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetMaxSize(window.InstanceHandle, maxWidth, maxHeight))); return window; } @@ -690,7 +690,7 @@ public static T SetMaxWidth(this T window, int maxWidth) where T : class, IIn /// InfiniFrame window instance public static T SetMinimized(this T window, bool minimized) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetMinimized({Minimized})", minimized); - window.Invoke(() => InfiniFrameNative.SetMinimized(window.InstanceHandle, minimized)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetMinimized(window.InstanceHandle, minimized))); return window; } @@ -705,7 +705,7 @@ public static T SetMinimized(this T window, bool minimized) where T : class, /// public static T SetMinSize(this T window, int minWidth, int minHeight) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetMinSize({MinWidth}, {MinHeight})", minWidth, minHeight); - window.Invoke(() => InfiniFrameNative.SetMinSize(window.InstanceHandle, minWidth, minHeight)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetMinSize(window.InstanceHandle, minWidth, minHeight))); return window; } @@ -756,11 +756,11 @@ public static T SetTitle(this T window, string? title) where T : class, IInfi window.Invoke(() => { IntPtr ptr = InfiniFrameNative.GetTitle(window.InstanceHandle); - string? oldTitle = InfiniFrameNative.PtrToNativeString(ptr); + string? oldTitle = InfiniFrameNative.PtrToNativeStringAndFree(ptr); if (title == oldTitle) return; if (OperatingSystem.IsLinux() && title?.Length > 31) title = title[..31];// Due to Linux/Gtk platform limitations, the window title has to be no more than 31 chars - InfiniFrameNative.SetTitle(window.InstanceHandle, title ?? string.Empty); + EnsureNative(InfiniFrameNative.SetTitle(window.InstanceHandle, title ?? string.Empty)); }); return window; @@ -778,10 +778,10 @@ public static T SetTitle(this T window, string? title) where T : class, IInfi public static T SetTop(this T window, int top) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetTop({Top})", top); window.Invoke(() => { - InfiniFrameNative.GetPosition(window.InstanceHandle, out int left, out int oldTop); + EnsureNative(InfiniFrameNative.GetPosition(window.InstanceHandle, out int left, out int oldTop)); if (top == oldTop) return; - InfiniFrameNative.SetPosition(window.InstanceHandle, left, top); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, left, top)); }); return window; @@ -798,7 +798,7 @@ public static T SetTop(this T window, int top) where T : class, IInfiniFrameW /// InfiniFrame window instance public static T SetTopMost(this T window, bool topMost) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetTopMost({TopMost})", topMost); - window.Invoke(() => InfiniFrameNative.SetTopmost(window.InstanceHandle, topMost)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetTopmost(window.InstanceHandle, topMost))); return window; } @@ -815,8 +815,8 @@ public static T SetWidth(this T window, int width) where T : class, IInfiniFr window.Logger.LogDebug(".SetWidth({Width})", width); window.Invoke(() => { - InfiniFrameNative.GetSize(window.InstanceHandle, out _, out int height); - InfiniFrameNative.SetSize(window.InstanceHandle, width, height); + EnsureNative(InfiniFrameNative.GetSize(window.InstanceHandle, out _, out int height)); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, width, height)); }); return window; @@ -834,7 +834,7 @@ public static T SetWidth(this T window, int width) where T : class, IInfiniFr /// 100 = 100%, 50 = 50% public static T SetZoom(this T window, int zoom) where T : class, IInfiniFrameWindow { window.Logger.LogDebug(".SetZoom({Zoom})", zoom); - window.Invoke(() => InfiniFrameNative.SetZoom(window.InstanceHandle, zoom)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetZoom(window.InstanceHandle, zoom))); return window; } @@ -855,7 +855,7 @@ public static T SetZoom(this T window, int zoom) where T : class, IInfiniFram public static T Win32SetWebView2Path(this T window, string data) where T : class, IInfiniFrameWindow { if (OperatingSystem.IsWindows()) window.Invoke(() - => InfiniFrameNative.SetWebView2RuntimePath_win32(window.NativeType, data)); + => EnsureNative(InfiniFrameNative.SetWebView2RuntimePath_win32(window.NativeType, data))); else window.Logger.LogDebug("Win32SetWebView2Path is only supported on the Windows platform"); @@ -875,7 +875,7 @@ public static T Win32SetWebView2Path(this T window, string data) where T : cl public static T ClearBrowserAutoFill(this T window) where T : class, IInfiniFrameWindow { if (OperatingSystem.IsWindows()) window.Invoke(() - => InfiniFrameNative.ClearBrowserAutoFill(window.InstanceHandle)); + => EnsureNative(InfiniFrameNative.ClearBrowserAutoFill(window.InstanceHandle))); else window.Logger.LogWarning("ClearBrowserAutoFill is only supported on the Windows platform"); @@ -894,8 +894,8 @@ public static T ClearBrowserAutoFill(this T window) where T : class, IInfiniF /// public static T Resize(this T window, int widthOffset, int heightOffset, ResizeOrigin origin) where T : class, IInfiniFrameWindow { window.Invoke(() => { - InfiniFrameNative.GetSize(window.InstanceHandle, out int width, out int height); - InfiniFrameNative.GetPosition(window.InstanceHandle, out int originalX, out int originalY); + EnsureNative(InfiniFrameNative.GetSize(window.InstanceHandle, out int width, out int height)); + EnsureNative(InfiniFrameNative.GetPosition(window.InstanceHandle, out int originalX, out int originalY)); int x = originalX; int y = originalY; @@ -977,8 +977,8 @@ public static T Resize(this T window, int widthOffset, int heightOffset, Resi y = originalY; } - InfiniFrameNative.SetSize(window.InstanceHandle, width, height); - InfiniFrameNative.SetPosition(window.InstanceHandle, x, y); + EnsureNative(InfiniFrameNative.SetSize(window.InstanceHandle, width, height)); + EnsureNative(InfiniFrameNative.SetPosition(window.InstanceHandle, x, y)); }); return window; @@ -993,7 +993,7 @@ public static T Resize(this T window, int widthOffset, int heightOffset, Resi /// Returns the current instance. /// public static T SetZoomEnabled(this T window, bool zoomEnabled) where T : class, IInfiniFrameWindow { - window.Invoke(() => InfiniFrameNative.SetZoomEnabled(window.InstanceHandle, zoomEnabled)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetZoomEnabled(window.InstanceHandle, zoomEnabled))); return window; } @@ -1008,7 +1008,10 @@ public static T SetZoomEnabled(this T window, bool zoomEnabled) where T : cla /// This method invokes the native function to set focus on the window instance. /// public static T SetFocused(this T window) where T : class, IInfiniFrameWindow { - window.Invoke(() => InfiniFrameNative.SetFocused(window.InstanceHandle)); + window.Invoke(() => EnsureNative(InfiniFrameNative.SetFocused(window.InstanceHandle))); return window; } + + private static void EnsureNative(InfiniFrameNativeStatusCode status) + => InfiniFrameNative.EnsureSucceeded(status); } diff --git a/src/InfiniFrame.Shared/Native/InfiniFrameNative.cs b/src/InfiniFrame.Shared/Native/InfiniFrameNative.cs index 8bbe1c83d..2c0e6104f 100644 --- a/src/InfiniFrame.Shared/Native/InfiniFrameNative.cs +++ b/src/InfiniFrame.Shared/Native/InfiniFrameNative.cs @@ -12,19 +12,28 @@ namespace InfiniFrame.Native; // Code // --------------------------------------------------------------------------------------------------------------------- public static partial class InfiniFrameNative { + internal static void EnsureSucceeded(InfiniFrameNativeStatusCode status) { + if (status == InfiniFrameNativeStatusCode.Success) return; + + string? nativeError = GetLastErrorMessageAndFree(); + string message = string.IsNullOrWhiteSpace(nativeError) + ? $"Native call failed with status {(int)status} ({status})." + : $"Native call failed with status {(int)status} ({status}). {nativeError}"; + throw new InvalidOperationException(message); + } #region MARSHAL CALLS FROM Non-UI Thread to UI Thread [LibraryImport(DllName, EntryPoint = InfiniFrame_Invoke, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Invoke(IntPtr instance, InvokeCallback callback); + internal static partial InfiniFrameNativeStatusCode Invoke(IntPtr instance, InvokeCallback callback); #endregion #region Register // ReSharper disable once UnusedMethodReturnValue.Local [LibraryImport(DllName, EntryPoint = InfiniFrame_register_win32, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void RegisterWin32(IntPtr hInstance); + internal static partial InfiniFrameNativeStatusCode RegisterWin32(IntPtr hInstance); // ReSharper disable once UnusedMethodReturnValue.Local [LibraryImport(DllName, EntryPoint = InfiniFrame_register_mac, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void RegisterMac(); + internal static partial InfiniFrameNativeStatusCode RegisterMac(); #endregion #region CTOR-DTOR @@ -32,13 +41,16 @@ public static partial class InfiniFrameNative { internal static partial IntPtr Constructor([MarshalUsing(typeof(InfiniFrameNativeParametersMarshaller))] in InfiniFrameNativeParameters parameters); [LibraryImport(DllName, EntryPoint = InfiniFrame_dtor), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Destructor(IntPtr instance); + internal static partial InfiniFrameNativeStatusCode Destructor(IntPtr instance); + + [LibraryImport(DllName, EntryPoint = InfiniFrame_GetLastErrorMessage, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial IntPtr GetLastErrorMessage(); [LibraryImport(DllName, EntryPoint = InfiniFrame_AddCustomSchemeName, SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void AddCustomSchemeName(IntPtr instance, string scheme); + internal static partial InfiniFrameNativeStatusCode AddCustomSchemeName(IntPtr instance, string scheme); [LibraryImport(DllName, EntryPoint = InfiniFrame_Close, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Close(IntPtr instance); + internal static partial InfiniFrameNativeStatusCode Close(IntPtr instance); #endregion #region Get @@ -46,175 +58,175 @@ public static partial class InfiniFrameNative { internal static partial IntPtr GetWindowHandlerWin32(IntPtr instance); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetAllMonitors, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetAllMonitors(IntPtr instance, CppGetAllMonitorsDelegate callback); + internal static partial InfiniFrameNativeStatusCode GetAllMonitors(IntPtr instance, CppGetAllMonitorsDelegate callback); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetTransparentEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetTransparentEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetTransparentEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetContextMenuEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetContextMenuEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetContextMenuEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetDevToolsEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetDevToolsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetDevToolsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetFullScreen, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetFullScreen(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool fullScreen); + internal static partial InfiniFrameNativeStatusCode GetFullScreen(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool fullScreen); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetGrantBrowserPermissions, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetGrantBrowserPermissions(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool grant); + internal static partial InfiniFrameNativeStatusCode GetGrantBrowserPermissions(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool grant); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetUserAgent, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial IntPtr GetUserAgent(IntPtr instance); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetMediaAutoplayEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetMediaAutoplayEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetMediaAutoplayEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetFileSystemAccessEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetFileSystemAccessEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetFileSystemAccessEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetWebSecurityEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetWebSecurityEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetWebSecurityEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetJavascriptClipboardAccessEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetJavascriptClipboardAccessEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetJavascriptClipboardAccessEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetMediaStreamEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetMediaStreamEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetMediaStreamEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetSmoothScrollingEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetSmoothScrollingEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetSmoothScrollingEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetIgnoreCertificateErrorsEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetIgnoreCertificateErrorsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetIgnoreCertificateErrorsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetNotificationsEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetNotificationsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); + internal static partial InfiniFrameNativeStatusCode GetNotificationsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetPosition, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetPosition(IntPtr instance, out int x, out int y); + internal static partial InfiniFrameNativeStatusCode GetPosition(IntPtr instance, out int x, out int y); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetResizable, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetResizable(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool resizable); + internal static partial InfiniFrameNativeStatusCode GetResizable(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool resizable); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetScreenDpi, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial uint GetScreenDpi(IntPtr instance); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetSize, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetSize(IntPtr instance, out int width, out int height); + internal static partial InfiniFrameNativeStatusCode GetSize(IntPtr instance, out int width, out int height); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetMaxSize, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetMaxSize(IntPtr instance, out int maxWidth, out int maxHeight); + internal static partial InfiniFrameNativeStatusCode GetMaxSize(IntPtr instance, out int maxWidth, out int maxHeight); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetMinSize, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetMinSize(IntPtr instance, out int minWidth, out int minHeight); + internal static partial InfiniFrameNativeStatusCode GetMinSize(IntPtr instance, out int minWidth, out int minHeight); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetTitle, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial IntPtr GetTitle(IntPtr instance); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetTopmost, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetTopmost(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool topmost); + internal static partial InfiniFrameNativeStatusCode GetTopmost(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool topmost); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetZoom, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetZoom(IntPtr instance, out int zoom); + internal static partial InfiniFrameNativeStatusCode GetZoom(IntPtr instance, out int zoom); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetMaximized, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetMaximized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool maximized); + internal static partial InfiniFrameNativeStatusCode GetMaximized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool maximized); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetMinimized, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetMinimized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool minimized); + internal static partial InfiniFrameNativeStatusCode GetMinimized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool minimized); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetZoomEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetZoomEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool zoomEnabled); + internal static partial InfiniFrameNativeStatusCode GetZoomEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool zoomEnabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetIconFileName, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial IntPtr GetIconFileName(IntPtr instance); [LibraryImport(DllName, EntryPoint = InfiniFrame_GetFocused, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetFocused(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool isFocused); + internal static partial InfiniFrameNativeStatusCode GetFocused(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool isFocused); #endregion #region Navigate [LibraryImport(DllName, EntryPoint = InfiniFrame_NavigateToString, SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void NavigateToString(IntPtr instance, string content); + internal static partial InfiniFrameNativeStatusCode NavigateToString(IntPtr instance, string content); [LibraryImport(DllName, EntryPoint = InfiniFrame_NavigateToUrl, SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void NavigateToUrl(IntPtr instance, string url); + internal static partial InfiniFrameNativeStatusCode NavigateToUrl(IntPtr instance, string url); #endregion #region Set [LibraryImport(DllName, EntryPoint = InfiniFrame_setWebView2RuntimePath_win32, SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetWebView2RuntimePath_win32(IntPtr instance, string webView2RuntimePath); + internal static partial InfiniFrameNativeStatusCode SetWebView2RuntimePath_win32(IntPtr instance, string webView2RuntimePath); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetTransparentEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetTransparentEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); + internal static partial InfiniFrameNativeStatusCode SetTransparentEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetContextMenuEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetContextMenuEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); + internal static partial InfiniFrameNativeStatusCode SetContextMenuEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetDevToolsEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetDevToolsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); + internal static partial InfiniFrameNativeStatusCode SetDevToolsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetFullScreen, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetFullScreen(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool fullScreen); + internal static partial InfiniFrameNativeStatusCode SetFullScreen(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool fullScreen); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetMaximized, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetMaximized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool maximized); + internal static partial InfiniFrameNativeStatusCode SetMaximized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool maximized); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetMaxSize, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetMaxSize(IntPtr instance, int maxWidth, int maxHeight); + internal static partial InfiniFrameNativeStatusCode SetMaxSize(IntPtr instance, int maxWidth, int maxHeight); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetMinimized, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetMinimized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool minimized); + internal static partial InfiniFrameNativeStatusCode SetMinimized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool minimized); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetMinSize, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetMinSize(IntPtr instance, int minWidth, int minHeight); + internal static partial InfiniFrameNativeStatusCode SetMinSize(IntPtr instance, int minWidth, int minHeight); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetResizable, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetResizable(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool resizable); + internal static partial InfiniFrameNativeStatusCode SetResizable(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool resizable); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetPosition, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetPosition(IntPtr instance, int x, int y); + internal static partial InfiniFrameNativeStatusCode SetPosition(IntPtr instance, int x, int y); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetSize, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetSize(IntPtr instance, int width, int height); + internal static partial InfiniFrameNativeStatusCode SetSize(IntPtr instance, int width, int height); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetTitle, SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetTitle(IntPtr instance, string title); + internal static partial InfiniFrameNativeStatusCode SetTitle(IntPtr instance, string title); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetTopmost, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetTopmost(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool topmost); + internal static partial InfiniFrameNativeStatusCode SetTopmost(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool topmost); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetIconFile, SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetIconFile(IntPtr instance, string filename); + internal static partial InfiniFrameNativeStatusCode SetIconFile(IntPtr instance, string filename); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetZoom, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetZoom(IntPtr instance, int zoom); + internal static partial InfiniFrameNativeStatusCode SetZoom(IntPtr instance, int zoom); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetZoomEnabled, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetZoomEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool zoomEnabled); + internal static partial InfiniFrameNativeStatusCode SetZoomEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool zoomEnabled); [LibraryImport(DllName, EntryPoint = InfiniFrame_SetFocused, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetFocused(IntPtr instance); + internal static partial InfiniFrameNativeStatusCode SetFocused(IntPtr instance); #endregion #region Misc [LibraryImport(DllName, EntryPoint = InfiniFrame_Center, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Center(IntPtr instance); + internal static partial InfiniFrameNativeStatusCode Center(IntPtr instance); [LibraryImport(DllName, EntryPoint = InfiniFrame_Restore, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Restore(IntPtr instance); + internal static partial InfiniFrameNativeStatusCode Restore(IntPtr instance); [LibraryImport(DllName, EntryPoint = InfiniFrame_ClearBrowserAutoFill, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void ClearBrowserAutoFill(IntPtr instance); + internal static partial InfiniFrameNativeStatusCode ClearBrowserAutoFill(IntPtr instance); [LibraryImport(DllName, EntryPoint = InfiniFrame_SendWebMessage, SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SendWebMessage(IntPtr instance, string message); + internal static partial InfiniFrameNativeStatusCode SendWebMessage(IntPtr instance, string message); [LibraryImport(DllName, EntryPoint = InfiniFrame_ShowNotification, SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void ShowNotification(IntPtr instance, string title, string body); + internal static partial InfiniFrameNativeStatusCode ShowNotification(IntPtr instance, string title, string body); [LibraryImport(DllName, EntryPoint = InfiniFrame_WaitForExit, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void WaitForExit(IntPtr instance); + internal static partial InfiniFrameNativeStatusCode WaitForExit(IntPtr instance); #endregion #region Dialog @@ -231,10 +243,10 @@ public static partial class InfiniFrameNative { internal static partial InfiniFrameDialogResult ShowMessage(IntPtr inst, string title, string text, InfiniFrameDialogButtons buttons, InfiniFrameDialogIcon icon); [LibraryImport(DllName, EntryPoint = InfiniFrame_FreeString, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void FreeString(IntPtr value); + internal static partial InfiniFrameNativeStatusCode FreeString(IntPtr value); [LibraryImport(DllName, EntryPoint = InfiniFrame_FreeStringArray, SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void FreeStringArray(IntPtr values, int count); + internal static partial InfiniFrameNativeStatusCode FreeStringArray(IntPtr values, int count); #endregion #region Overloads @@ -246,59 +258,107 @@ public static partial class InfiniFrameNative { : Marshal.PtrToStringUTF8(ptr); } - internal static void GetHeight(IntPtr instance, out int height) => GetSize(instance, out _, out height); - internal static void GetWidth(IntPtr instance, out int width) => GetSize(instance, out width, out _); - internal static void GetMaxHeight(IntPtr instance, out int maxHeight) => GetMaxSize(instance, out _, out maxHeight); - internal static void GetMaxWidth(IntPtr instance, out int maxWidth) => GetMaxSize(instance, out maxWidth, out _); - internal static void GetMinHeight(IntPtr instance, out int minHeight) => GetMinSize(instance, out _, out minHeight); - internal static void GetMinWidth(IntPtr instance, out int minWidth) => GetMinSize(instance, out minWidth, out _); + internal static string? PtrToNativeStringAndFree(IntPtr ptr) { + if (ptr == IntPtr.Zero) return null; - internal static void GetLeft(IntPtr instance, out int left) => GetPosition(instance, out left, out _); - internal static void GetTop(IntPtr instance, out int top) => GetPosition(instance, out _, out top); + try { + return PtrToNativeString(ptr); + } + finally { + FreeString(ptr); + } + } - internal static void GetSize(IntPtr instance, out Size size) { - GetSize(instance, out int width, out int height); + internal static string? GetLastErrorMessageAndFree() { + IntPtr ptr = GetLastErrorMessage(); + return PtrToNativeStringAndFree(ptr); + } + + internal static InfiniFrameNativeStatusCode GetHeight(IntPtr instance, out int height) => GetSize(instance, out _, out height); + internal static InfiniFrameNativeStatusCode GetWidth(IntPtr instance, out int width) => GetSize(instance, out width, out _); + internal static InfiniFrameNativeStatusCode GetMaxHeight(IntPtr instance, out int maxHeight) => GetMaxSize(instance, out _, out maxHeight); + internal static InfiniFrameNativeStatusCode GetMaxWidth(IntPtr instance, out int maxWidth) => GetMaxSize(instance, out maxWidth, out _); + internal static InfiniFrameNativeStatusCode GetMinHeight(IntPtr instance, out int minHeight) => GetMinSize(instance, out _, out minHeight); + internal static InfiniFrameNativeStatusCode GetMinWidth(IntPtr instance, out int minWidth) => GetMinSize(instance, out minWidth, out _); + + internal static InfiniFrameNativeStatusCode GetLeft(IntPtr instance, out int left) => GetPosition(instance, out left, out _); + internal static InfiniFrameNativeStatusCode GetTop(IntPtr instance, out int top) => GetPosition(instance, out _, out top); + + internal static InfiniFrameNativeStatusCode GetSize(IntPtr instance, out Size size) { + InfiniFrameNativeStatusCode status = GetSize(instance, out int width, out int height); size = new Size(width, height); + return status; } - internal static void GetMaxSize(IntPtr instance, out Size size) { - GetMaxSize(instance, out int width, out int height); + internal static InfiniFrameNativeStatusCode GetMaxSize(IntPtr instance, out Size size) { + InfiniFrameNativeStatusCode status = GetMaxSize(instance, out int width, out int height); size = new Size(width, height); + return status; } - internal static void GetMinSize(IntPtr instance, out Size size) { - GetMinSize(instance, out int width, out int height); + internal static InfiniFrameNativeStatusCode GetMinSize(IntPtr instance, out Size size) { + InfiniFrameNativeStatusCode status = GetMinSize(instance, out int width, out int height); size = new Size(width, height); + return status; } - internal static void GetPosition(IntPtr instance, out Point position) { - GetPosition(instance, out int left, out int top); + internal static InfiniFrameNativeStatusCode GetPosition(IntPtr instance, out Point position) { + InfiniFrameNativeStatusCode status = GetPosition(instance, out int left, out int top); position = new Point(left, top); + return status; } - internal static void GetWindowRectangle(IntPtr instance, out int x, out int y, out int width, out int height) { - GetSize(instance, out width, out height); - GetPosition(instance, out x, out y); + internal static InfiniFrameNativeStatusCode GetWindowRectangle(IntPtr instance, out int x, out int y, out int width, out int height) { + InfiniFrameNativeStatusCode sizeStatus = GetSize(instance, out width, out height); + if (sizeStatus != InfiniFrameNativeStatusCode.Success) { + x = 0; + y = 0; + return sizeStatus; + } + + return GetPosition(instance, out x, out y); } - internal static void GetWindowRectangle(IntPtr instance, out Rectangle rectangle) { - GetWindowRectangle(instance, out int x, out int y, out int width, out int height); + internal static InfiniFrameNativeStatusCode GetWindowRectangle(IntPtr instance, out Rectangle rectangle) { + InfiniFrameNativeStatusCode status = GetWindowRectangle(instance, out int x, out int y, out int width, out int height); rectangle = new Rectangle(x, y, width, height); + return status; } - internal static void GetUserAgent(IntPtr instance, out string? userAgent) { + internal static InfiniFrameNativeStatusCode GetUserAgent(IntPtr instance, out string? userAgent) { IntPtr ptr = GetUserAgent(instance); - userAgent = PtrToNativeString(ptr); + InfiniFrameNativeStatusCode status = (InfiniFrameNativeStatusCode)Marshal.GetLastPInvokeError(); + if (status != InfiniFrameNativeStatusCode.Success) { + userAgent = null; + return status; + } + + userAgent = PtrToNativeStringAndFree(ptr); + return status; } - internal static void GetTitle(IntPtr instance, out string title) { + internal static InfiniFrameNativeStatusCode GetTitle(IntPtr instance, out string title) { IntPtr ptr = GetTitle(instance); - title = PtrToNativeString(ptr) ?? string.Empty;// The way on how infiniFrame works internally is that the title is always an empty string when we set it to null on our end. + InfiniFrameNativeStatusCode status = (InfiniFrameNativeStatusCode)Marshal.GetLastPInvokeError(); + if (status != InfiniFrameNativeStatusCode.Success) { + title = string.Empty; + return status; + } + + title = PtrToNativeStringAndFree(ptr) ?? string.Empty;// The way on how infiniFrame works internally is that the title is always an empty string when we set it to null on our end. + return status; } - internal static void GetIconFileName(IntPtr instance, out string iconFileName) { + internal static InfiniFrameNativeStatusCode GetIconFileName(IntPtr instance, out string iconFileName) { IntPtr ptr = GetIconFileName(instance); - iconFileName = PtrToNativeString(ptr) ?? string.Empty; + InfiniFrameNativeStatusCode status = (InfiniFrameNativeStatusCode)Marshal.GetLastPInvokeError(); + if (status != InfiniFrameNativeStatusCode.Success) { + iconFileName = string.Empty; + return status; + } + + iconFileName = PtrToNativeStringAndFree(ptr) ?? string.Empty; + return status; } #endregion } diff --git a/src/InfiniFrame.Shared/Native/InfiniFrameNativeStatusCode.cs b/src/InfiniFrame.Shared/Native/InfiniFrameNativeStatusCode.cs new file mode 100644 index 000000000..a53c46796 --- /dev/null +++ b/src/InfiniFrame.Shared/Native/InfiniFrameNativeStatusCode.cs @@ -0,0 +1,7 @@ +namespace InfiniFrame.Native; + +internal enum InfiniFrameNativeStatusCode : int { + Success = 0, + InvalidArgument = 22, + OperationFailed = 14 +} diff --git a/src/InfiniFrame.Shared/Native/NativeDll.cs b/src/InfiniFrame.Shared/Native/NativeDll.cs index bec2695fc..3d0265a52 100644 --- a/src/InfiniFrame.Shared/Native/NativeDll.cs +++ b/src/InfiniFrame.Shared/Native/NativeDll.cs @@ -14,6 +14,7 @@ internal static class NativeDll { internal const string InfiniFrame_register_mac = nameof(InfiniFrame_register_mac); internal const string InfiniFrame_ctor = nameof(InfiniFrame_ctor); internal const string InfiniFrame_dtor = nameof(InfiniFrame_dtor); + internal const string InfiniFrame_GetLastErrorMessage = nameof(InfiniFrame_GetLastErrorMessage); internal const string InfiniFrame_AddCustomSchemeName = nameof(InfiniFrame_AddCustomSchemeName); internal const string InfiniFrame_Close = nameof(InfiniFrame_Close); internal const string InfiniFrame_getHwnd_win32 = nameof(InfiniFrame_getHwnd_win32); diff --git a/src/InfiniFrame.Shared/Utilities/InvokeUtilities.cs b/src/InfiniFrame.Shared/Utilities/InvokeUtilities.cs index 1849f44eb..beacad351 100644 --- a/src/InfiniFrame.Shared/Utilities/InvokeUtilities.cs +++ b/src/InfiniFrame.Shared/Utilities/InvokeUtilities.cs @@ -2,6 +2,7 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- using System.Diagnostics; +using InfiniFrame.Native; namespace InfiniFrame.Utilities; // --------------------------------------------------------------------------------------------------------------------- @@ -41,17 +42,17 @@ internal static class InvokeUtilities { return value; } - public static T InvokeAndReturn(IInfiniFrameWindow window, FuncWithOut callback) { + public static T InvokeAndReturn(IInfiniFrameWindow window, StatusFuncWithOut callback) { T? value = default; // ReSharper disable once RedundantAssignment bool completed = false; window.Invoke(() => { - callback(window.InstanceHandle, out value); + InfiniFrameNative.EnsureSucceeded(callback(window.InstanceHandle, out value)); completed = true; }); Debug.Assert(completed, "Invoke must be synchronous — callback did not complete before Invoke returned."); return value!; } - internal delegate void FuncWithOut(IntPtr handle, out T value); + internal delegate InfiniFrameNativeStatusCode StatusFuncWithOut(IntPtr handle, out T value); } diff --git a/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs b/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs index 32ccdceb1..64d80fe9e 100644 --- a/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs +++ b/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs @@ -13,7 +13,7 @@ internal static class MonitorsUtility { public static ImmutableArray GetMonitors(IInfiniFrameWindow window) { ImmutableArray.Builder builder = ImmutableArray.CreateBuilder(); - InfiniFrameNative.GetAllMonitors(window.InstanceHandle, Callback); + InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.GetAllMonitors(window.InstanceHandle, Callback)); return builder.ToImmutable(); int Callback(in NativeMonitor monitor) { @@ -82,7 +82,7 @@ public static bool TryGetCurrentMonitor(ImmutableArray monitors, public static bool TryGetCurrentWindowAndMonitor(IInfiniFrameWindow window, out Rectangle windowRect, out InfiniMonitor monitor) { ImmutableArray monitors = GetMonitors(window); - InfiniFrameNative.GetWindowRectangle(window.InstanceHandle, out windowRect); + InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.GetWindowRectangle(window.InstanceHandle, out windowRect)); return TryGetCurrentMonitor(monitors, windowRect, out monitor); } } diff --git a/src/InfiniFrame/InfiniFrameWindow.cs b/src/InfiniFrame/InfiniFrameWindow.cs index e35d7b5d8..9ab45443a 100644 --- a/src/InfiniFrame/InfiniFrameWindow.cs +++ b/src/InfiniFrame/InfiniFrameWindow.cs @@ -49,7 +49,7 @@ public sealed class InfiniFrameWindow : IInfiniFrameWindow { public void Invoke(Action workItem) { // If we're already on the UI thread, no need to dispatch if (Environment.CurrentManagedThreadId == ManagedThreadId) workItem(); - else InfiniFrameNative.Invoke(InstanceHandle, workItem.Invoke); + else InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.Invoke(InstanceHandle, workItem.Invoke)); } /// @@ -63,15 +63,14 @@ public void Invoke(Action workItem) { public void WaitForClose() { try { Logger.LogDebug("Starting message loop for window."); - Invoke(() => InfiniFrameNative.WaitForExit(InstanceHandle)); + Invoke(() => InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.WaitForExit(InstanceHandle))); } catch (Exception ex) when (IsNonFatalException(ex)) { - int lastError = 0; - if (OperatingSystem.IsWindows()) - lastError = Marshal.GetLastWin32Error(); + int lastError = Marshal.GetLastPInvokeError(); + string? nativeError = InfiniFrameNative.GetLastErrorMessageAndFree(); - Logger.LogError(ex, "Error #{LastErrorCode} while running message loop", lastError); - throw new ApplicationException($"Native code exception. Error # {lastError} See inner exception for details.", ex); + Logger.LogError(ex, "Error #{LastErrorCode} while running message loop: {NativeError}", lastError, nativeError); + throw new ApplicationException(CreateNativeExceptionMessage("Native code exception", lastError, nativeError), ex); } finally { Interlocked.Exchange(ref _shutdownRequested, 1); @@ -106,7 +105,7 @@ public void Close() { return; } - InfiniFrameNative.Close(InstanceHandle); + InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.Close(InstanceHandle)); }); } @@ -135,7 +134,7 @@ public void SendWebMessage(string message) { return; } - InfiniFrameNative.SendWebMessage(InstanceHandle, message); + InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.SendWebMessage(InstanceHandle, message)); }); } @@ -168,7 +167,7 @@ public void SendNotification(string title, string body) { return; } - InfiniFrameNative.ShowNotification(InstanceHandle, title, body); + InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.ShowNotification(InstanceHandle, title, body)); }); } @@ -326,7 +325,7 @@ public IInfiniFrameWindow RegisterCustomSchemeHandler(string scheme, NetCustomSc scheme = scheme.ToLower(); - InfiniFrameNative.AddCustomSchemeName(InstanceHandle, scheme); + InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.AddCustomSchemeName(InstanceHandle, scheme)); CustomSchemes.RegisterCustomSchemeHandler(scheme, handler); return this; @@ -387,19 +386,24 @@ out string? resolvedIconFilePath // All C++ exceptions will bubble up to here. try { if (OperatingSystem.IsWindows()) - Invoke(() => InfiniFrameNative.RegisterWin32(NativeType)); + Invoke(() => InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.RegisterWin32(NativeType))); else if (OperatingSystem.IsMacOS()) - Invoke(InfiniFrameNative.RegisterMac); + Invoke(() => InfiniFrameNative.EnsureSucceeded(InfiniFrameNative.RegisterMac())); Invoke(() => InstanceHandle = InfiniFrameNative.Constructor(in StartupParameters)); + + if (InstanceHandle == IntPtr.Zero) { + int lastError = Marshal.GetLastPInvokeError(); + string? nativeError = InfiniFrameNative.GetLastErrorMessageAndFree(); + throw new ApplicationException(CreateNativeExceptionMessage("Native window creation failed", lastError, nativeError)); + } } - catch (Exception ex) when (IsNonFatalException(ex)) { - int lastError = 0; - if (OperatingSystem.IsWindows()) - lastError = Marshal.GetLastWin32Error(); + catch (Exception ex) when (IsNonFatalException(ex) && ex is not ApplicationException) { + int lastError = Marshal.GetLastPInvokeError(); + string? nativeError = InfiniFrameNative.GetLastErrorMessageAndFree(); - Logger.LogError(ex, "Error #{LastErrorCode} while creating native window", lastError); - throw new ApplicationException($"Native code exception. Error # {lastError} See inner exception for details.", ex); + Logger.LogError(ex, "Error #{LastErrorCode} while creating native window: {NativeError}", lastError, nativeError); + throw new ApplicationException(CreateNativeExceptionMessage("Native code exception", lastError, nativeError), ex); } Events.OnWindowCreated(); @@ -866,5 +870,10 @@ public IntPtr OnCustomScheme(string url, out int numBytes, out string? contentTy private static bool IsNonFatalException(Exception exception) => exception is not (OutOfMemoryException or AccessViolationException); + + private static string CreateNativeExceptionMessage(string prefix, int lastError, string? nativeError) + => string.IsNullOrWhiteSpace(nativeError) + ? $"{prefix}. Error # {lastError}." + : $"{prefix}. Error # {lastError}. {nativeError}"; #endregion } diff --git a/tests/InfiniFrameTests/InfiniFrameNativeExportGuardTests.cs b/tests/InfiniFrameTests/InfiniFrameNativeExportGuardTests.cs new file mode 100644 index 000000000..93f53a57d --- /dev/null +++ b/tests/InfiniFrameTests/InfiniFrameNativeExportGuardTests.cs @@ -0,0 +1,95 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using InfiniFrame.Native; +using System.Runtime.InteropServices; + +namespace InfiniFrameTests; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class InfiniFrameNativeExportGuardTests { + private const int InvalidArgument = 22; + + [Test] + public async Task NullWindowHandle_ReturnsSafeDefaultsAndSetsLastError() { + // Act + InfiniFrameNative.GetSize(IntPtr.Zero, out int width, out int height); + int lastError = Marshal.GetLastPInvokeError(); + IntPtr title = InfiniFrameNative.GetTitle(IntPtr.Zero); + int titleLastError = Marshal.GetLastPInvokeError(); + + // Assert + await Assert.That(width).IsEqualTo(0); + await Assert.That(height).IsEqualTo(0); + await Assert.That(lastError).IsEqualTo(InvalidArgument); + await Assert.That(title).IsEqualTo(IntPtr.Zero); + await Assert.That(titleLastError).IsEqualTo(InvalidArgument); + } + + [Test] + public async Task PtrToNativeStringAndFree_ReturnsNullForZeroPointer() { + // Act + string? value = InfiniFrameNative.PtrToNativeStringAndFree(IntPtr.Zero); + + // Assert + await Assert.That(value).IsNull(); + } + + [Test] + public async Task SuccessfulNoOpExport_ClearsPreviousLastError() { + // Arrange + _ = InfiniFrameNative.GetTitle(IntPtr.Zero); + await Assert.That(Marshal.GetLastPInvokeError()).IsEqualTo(InvalidArgument); + + // Act + InfiniFrameNativeStatusCode status = InfiniFrameNative.FreeString(IntPtr.Zero); + + // Assert + await Assert.That(status).IsEqualTo(InfiniFrameNativeStatusCode.Success); + await Assert.That(Marshal.GetLastPInvokeError()).IsEqualTo(0); + } + + [Test] + public async Task StatusExport_WithNullWindow_ReturnsInvalidArgumentAndSetsLastError() { + // Act + InfiniFrameNativeStatusCode status = InfiniFrameNative.Center(IntPtr.Zero); + + // Assert + await Assert.That(status).IsEqualTo(InfiniFrameNativeStatusCode.InvalidArgument); + await Assert.That(Marshal.GetLastPInvokeError()).IsEqualTo(InvalidArgument); + } + + [Test] + public async Task Constructor_WithInvalidInitParameterSize_ReturnsNullAndSetsInvalidArgument() { + // Arrange + var parameters = new InfiniFrameNativeParameters { + StartString = "", + Size = 1 + }; + + // Act + IntPtr instance = InfiniFrameNative.Constructor(in parameters); + + // Assert + await Assert.That(instance).IsEqualTo(IntPtr.Zero); + await Assert.That(Marshal.GetLastPInvokeError()).IsEqualTo(InvalidArgument); + } + + [Test] + public async Task Constructor_WithInvalidInitParameterSize_PreservesNativeErrorMessage() { + // Arrange + var parameters = new InfiniFrameNativeParameters { + StartString = "", + Size = 1 + }; + + // Act + _ = InfiniFrameNative.Constructor(in parameters); + string? message = InfiniFrameNative.GetLastErrorMessageAndFree(); + + // Assert + await Assert.That(message).Contains("Initial parameters passed are 1 bytes"); + } +}