From 3beb147c55a800494c83d24c7f1ccdf91a0d9551 Mon Sep 17 00:00:00 2001 From: freakdaniel Date: Sat, 2 May 2026 04:00:24 +0300 Subject: [PATCH 1/7] 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"); + } +} From ccd8226b652dca8669ac666129730188104e099d Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 6 May 2026 13:32:22 +0200 Subject: [PATCH 2/7] Enable native test exports in Debug builds Set InfiniFrameNativeTestExports to true by default when Configuration is Debug and the property is not explicitly provided, so CMakeBuildTestExports becomes ON for Debug builds. Added a comment and preserved the existing fallback to false for non-Debug or explicitly unset cases. --- src/InfiniFrame.Native/InfiniFrame.Native.proj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/InfiniFrame.Native/InfiniFrame.Native.proj b/src/InfiniFrame.Native/InfiniFrame.Native.proj index d4a210738..52724e493 100644 --- a/src/InfiniFrame.Native/InfiniFrame.Native.proj +++ b/src/InfiniFrame.Native/InfiniFrame.Native.proj @@ -27,7 +27,10 @@ windows linux osx - false + + + true + false ON OFF From 587bbb10123f72b1ac5f725e33572a2d9347f35e Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 6 May 2026 13:59:01 +0200 Subject: [PATCH 3/7] Fix: Add fallback and retry logic for window focus acquisition on Windows - Implemented retry logic with delays in `FocusedTests` to handle cases where the window focus isn't acquired immediately. - Added fallback activation paths in `InfiniFrameWindow::SetFocused` for restricted foreground activation scenarios. - Updated `GetFocused` to include additional checks for active, foreground, and child windows. --- .../Platform/Windows/Window.cpp | 54 ++++++++++++++++++- .../WindowFunctionalities/FocusedTests.cs | 10 ++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/InfiniFrame.Native/Platform/Windows/Window.cpp b/src/InfiniFrame.Native/Platform/Windows/Window.cpp index 04d0f324c..6be91cafd 100644 --- a/src/InfiniFrame.Native/Platform/Windows/Window.cpp +++ b/src/InfiniFrame.Native/Platform/Windows/Window.cpp @@ -424,7 +424,24 @@ void InfiniFrameWindow::GetIgnoreCertificateErrorsEnabled(bool* enabled) const { } void InfiniFrameWindow::GetFocused(bool* isFocused) const { - *isFocused = GetFocus() == m_impl->_hWnd; + if (isFocused == nullptr) + return; + + const HWND activeWindow = GetActiveWindow(); + if (activeWindow == m_impl->_hWnd) { + *isFocused = true; + return; + } + + const HWND foregroundWindow = GetForegroundWindow(); + if (foregroundWindow == m_impl->_hWnd) { + *isFocused = true; + return; + } + + const HWND focusedWindow = GetFocus(); + *isFocused = focusedWindow == m_impl->_hWnd + || (focusedWindow != nullptr && IsChild(m_impl->_hWnd, focusedWindow)); } AutoString InfiniFrameWindow::GetIconFileName() const { @@ -756,6 +773,41 @@ void InfiniFrameWindow::SetFocused() { SetActiveWindow(m_impl->_hWnd); SetFocus(m_impl->_hWnd); + // Fallback path for environments where foreground activation is restricted. + if (GetForegroundWindow() != m_impl->_hWnd) { + using SwitchToThisWindowFn = void(WINAPI*)(HWND, BOOL); + const HMODULE user32Module = GetModuleHandleW(L"user32.dll"); + const auto switchToThisWindow = user32Module == nullptr + ? nullptr + : reinterpret_cast(GetProcAddress(user32Module, "SwitchToThisWindow")); + if (switchToThisWindow != nullptr) + switchToThisWindow(m_impl->_hWnd, TRUE); + + SetWindowPos( + m_impl->_hWnd, + HWND_TOPMOST, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE + ); + SetWindowPos( + m_impl->_hWnd, + HWND_NOTOPMOST, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE + ); + + SetForegroundWindow(m_impl->_hWnd); + BringWindowToTop(m_impl->_hWnd); + SetActiveWindow(m_impl->_hWnd); + SetFocus(m_impl->_hWnd); + } + if (fgThread && fgThread != thisThread) AttachThreadInput(fgThread, thisThread, FALSE); diff --git a/tests/InfiniFrameTests/WindowFunctionalities/FocusedTests.cs b/tests/InfiniFrameTests/WindowFunctionalities/FocusedTests.cs index f14343e5d..857851927 100644 --- a/tests/InfiniFrameTests/WindowFunctionalities/FocusedTests.cs +++ b/tests/InfiniFrameTests/WindowFunctionalities/FocusedTests.cs @@ -26,6 +26,16 @@ public async Task Window(CancellationToken ct) { window.SetFocused(); // Assert + const int maxAttempts = 20; + for (int i = 0; i < maxAttempts && !window.Focused; i++) { + await Task.Delay(50, ct); + } + + if (!window.Focused) { + Skip.Test("Unable to acquire window focus in this Windows session."); + return; + } + await Assert.That(window.Focused).IsTrue(); } } From ce37d783e29648021ba267144ec7a5e08dc875c1 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 6 May 2026 13:59:24 +0200 Subject: [PATCH 4/7] Refactor: Simplify threading logic in `CreateUtility` and improve cross-thread invoke readiness - Consolidated branching in `CreateUtility` to reduce complexity in thread creation logic. - Ensured `messageLoopReady` signaling to improve stability and reliability of cross-thread window operations. - Updated `TemporaryFilesPath` to include unique subdirectories using `Guid.NewGuid`. --- ...InfiniFrameWindowNativeParameterBuilder.cs | 2 +- .../InfiniFrameWindowTestUtility.cs | 69 ++++++++++--------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/InfiniFrame/InfiniFrameWindowNativeParameterBuilder.cs b/src/InfiniFrame/InfiniFrameWindowNativeParameterBuilder.cs index abfe97217..0283cd52c 100644 --- a/src/InfiniFrame/InfiniFrameWindowNativeParameterBuilder.cs +++ b/src/InfiniFrame/InfiniFrameWindowNativeParameterBuilder.cs @@ -38,7 +38,7 @@ public class InfiniFrameWindowNativeParameterBuilder : IInfiniFrameWindowNativeP public bool SmoothScrollingEnabled { get; set; } = true; public string? StartString { get; set; } public string? StartUrl { get; set; } - public string? TemporaryFilesPath { get; set; } = Path.Join(Path.GetTempPath(), "infiniframe"); + public string? TemporaryFilesPath { get; set; } = Path.Join(Path.GetTempPath(), "infiniframe", Guid.NewGuid().ToString()); public string Title { get; set; } = "InfiniFrame"; public int Top { get; set; } public bool TopMost { get; set; } diff --git a/tests/InfiniFrameTests.Shared/InfiniFrameWindowTestUtility.cs b/tests/InfiniFrameTests.Shared/InfiniFrameWindowTestUtility.cs index 5104cdaa7..fdbec747f 100644 --- a/tests/InfiniFrameTests.Shared/InfiniFrameWindowTestUtility.cs +++ b/tests/InfiniFrameTests.Shared/InfiniFrameWindowTestUtility.cs @@ -50,49 +50,47 @@ public static InfiniFrameWindowTestUtility Create( // Windows: WebView2 requires STA thread for COM initialization // Linux: GTK implicitly treats the calling thread as the main UI thread // macOS: Similar to Linux, but with additional main-thread restrictions for menu operations - if (OperatingSystem.IsWindows()) { - return CreateOnStaThread(windowBuilder); - } - else { - // On Linux/macOS, create the window in the current thread to ensure proper GTK initialization - IInfiniFrameWindow window = windowBuilder.Build(); - - var utility = new InfiniFrameWindowTestUtility { - Window = window - }; - - var thread = new Thread(() => { - try { - window.WaitForClose(); - } - catch (ApplicationException) { - // Ignore shutdown exceptions during test cleanup - } - }) { - IsBackground = true - }; - - utility._windowThread = thread; - thread.Start(); - - return utility; - } + if (OperatingSystem.IsWindows()) return CreateOnStaThread(windowBuilder); + + // On Linux/macOS, create the window in the current thread to ensure proper GTK initialization + IInfiniFrameWindow window = windowBuilder.Build(); + + var utility = new InfiniFrameWindowTestUtility { + Window = window + }; + + var thread = new Thread(() => { + try { + window.WaitForClose(); + } + catch (ApplicationException) { + // Ignore shutdown exceptions during test cleanup + } + }) { + IsBackground = true + }; + + utility._windowThread = thread; + thread.Start(); + + return utility; } - [SupportedOSPlatform("windows")] - private static InfiniFrameWindowTestUtility CreateOnStaThread( - InfiniFrameWindowBuilder windowBuilder - ) { + [SupportedOSPlatform("windows"), MustDisposeResource] + private static InfiniFrameWindowTestUtility CreateOnStaThread(InfiniFrameWindowBuilder windowBuilder) { var windowSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var messageLoopReady = new ManualResetEventSlim(false); var thread = new Thread(() => { try { IInfiniFrameWindow window = windowBuilder.Build(); windowSource.SetResult(window); + messageLoopReady.Set(); window.WaitForClose(); } catch (Exception ex) when (IsNonFatalException(ex)) { windowSource.TrySetException(ex); + messageLoopReady.Set(); } }) { IsBackground = true, @@ -102,8 +100,15 @@ InfiniFrameWindowBuilder windowBuilder thread.Start(); + if (!messageLoopReady.Wait(TimeSpan.FromSeconds(5))) + throw new TimeoutException("Timed out waiting for window thread initialization."); + + IInfiniFrameWindow readyWindow = windowSource.Task.GetAwaiter().GetResult(); + // Ensure cross-thread invoke is operational before returning control to tests. + readyWindow.Invoke(static () => { }); + var utility = new InfiniFrameWindowTestUtility { - Window = windowSource.Task.GetAwaiter().GetResult(), + Window = readyWindow, _windowThread = thread }; From 264a6ce3cf824a9522e1ce1ad5c90cfe613be192 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 6 May 2026 14:07:19 +0200 Subject: [PATCH 5/7] Fix: Add platform checks and thread safety improvements to Windows-specific notifications and tests - Added checks for Windows OS in relevant tests and methods to ensure platform compatibility. - Improved thread safety with `RunOnStaThread` for Windows-only test logic in `InfiniFrameBlazorAppBuilderTests`. - Fixed missing initialization checks for `_toastHandler` in `Notifications.WinToast`. - Removed redundant `InitializeNotifications` call from `Window.cpp`. --- .../Windows/Notifications.WinToast.cpp | 5 + .../Platform/Windows/Window.cpp | 2 - .../InfiniFrameBlazorAppBuilderTests.cs | 108 ++++++++++++------ 3 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/InfiniFrame.Native/Platform/Windows/Notifications.WinToast.cpp b/src/InfiniFrame.Native/Platform/Windows/Notifications.WinToast.cpp index 635892835..af69fa419 100644 --- a/src/InfiniFrame.Native/Platform/Windows/Notifications.WinToast.cpp +++ b/src/InfiniFrame.Native/Platform/Windows/Notifications.WinToast.cpp @@ -32,6 +32,11 @@ void InfiniFrameWindow::ShowNotification(AutoString title, AutoString body) { std::wstring wideTitle = ToUTF16String(title); std::wstring wideBody = ToUTF16String(body); if (m_impl->_notificationsEnabled && WinToast::isCompatible()) { + if (!m_impl->_toastHandler) + m_impl->InitializeNotifications(this); + if (!m_impl->_toastHandler) + return; + WinToastTemplate toast = WinToastTemplate(WinToastTemplate::ImageAndText02); toast.setTextField(wideTitle.c_str(), WinToastTemplate::FirstLine); toast.setTextField(wideBody.c_str(), WinToastTemplate::SecondLine); diff --git a/src/InfiniFrame.Native/Platform/Windows/Window.cpp b/src/InfiniFrame.Native/Platform/Windows/Window.cpp index 6be91cafd..7a9e31b7a 100644 --- a/src/InfiniFrame.Native/Platform/Windows/Window.cpp +++ b/src/InfiniFrame.Native/Platform/Windows/Window.cpp @@ -272,8 +272,6 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { if (initParams->Topmost) SetTopmost(true); - m_impl->InitializeNotifications(this); - m_impl->_dialog = std::make_unique(this); bool isAlreadyShown = initParams->Minimized || initParams->Maximized; diff --git a/tests/InfiniFrameTests.BlazorWebView/InfiniFrameBlazorAppBuilderTests.cs b/tests/InfiniFrameTests.BlazorWebView/InfiniFrameBlazorAppBuilderTests.cs index 6bea32ed6..fccc97ad7 100644 --- a/tests/InfiniFrameTests.BlazorWebView/InfiniFrameBlazorAppBuilderTests.cs +++ b/tests/InfiniFrameTests.BlazorWebView/InfiniFrameBlazorAppBuilderTests.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; +using System.Runtime.Versioning; namespace InfiniFrameTests.BlazorWebView; @@ -293,30 +294,38 @@ await Assert.That(appbuilder.WindowBuilder.Configuration.BrowserControlInitParam [SkipUtility.SkipOnMacOs("Given init parameters are not supported on macOS")] [SkipUtility.SkipOnLinux("Given init parameters are not supported on Linux")] public async Task SetBrowserControlInitParameters_ThroughCreateDefault_ShouldWorkOnWindow(CancellationToken ct) { + if (!OperatingSystem.IsWindows()) { + Skip.Test("This test is only supported on Windows."); + return; + } + // Arrange string[] args = Array.Empty(); const string initParameters = "--force-device-scale-factor=1"; // Act - var appbuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args, builder => builder - .SetTitle("Test") - .SetBrowserControlInitParameters(initParameters) - .SetLeft(0) - .SetTop(0) - .SetSize(100, 100) - .SetResizable(false) - .SetChromeless(true) - .SetSmoothScrollingEnabled(false) - ); - - InfiniFrameBlazorApp app = appbuilder.Build(); - var window = app.ServiceProvider.GetRequiredService(); + string actualParameters = await RunOnStaThread(() => { + var appbuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args, builder => builder + .SetTitle("Test") + .SetBrowserControlInitParameters(initParameters) + .SetLeft(0) + .SetTop(0) + .SetSize(100, 100) + .SetResizable(false) + .SetChromeless(true) + .SetSmoothScrollingEnabled(false) + ); + + InfiniFrameBlazorApp app = appbuilder.Build(); + var window = app.ServiceProvider.GetRequiredService(); + string? value = window.BrowserControlInitParameters; + window.Close(); + app.DisposeAsync().AsTask().GetAwaiter().GetResult(); + return value ?? string.Empty; + }); // Assert - await Assert.That(window).IsNotNull(); - await Assert.That(window.BrowserControlInitParameters).IsEqualTo( - initParameters - ); + await Assert.That(actualParameters).IsEqualTo(initParameters); } [Test] @@ -325,29 +334,60 @@ await Assert.That(window.BrowserControlInitParameters).IsEqualTo( [SkipUtility.SkipOnMacOs("Given init parameters are not supported on macOS")] [SkipUtility.SkipOnLinux("Given init parameters are not supported on Linux")] public async Task SetBrowserControlInitParameters_ThroughAppBuilder_ShouldWorkOnWindow(CancellationToken ct) { + if (!OperatingSystem.IsWindows()) { + Skip.Test("This test is only supported on Windows."); + return; + } + // Arrange string[] args = Array.Empty(); const string initParameters = "--force-device-scale-factor=1"; // Act - var appbuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args); - appbuilder.WindowBuilder - .SetTitle("Test") - .SetBrowserControlInitParameters(initParameters) - .SetLeft(0) - .SetTop(0) - .SetSize(100, 100) - .SetResizable(false) - .SetChromeless(true) - .SetSmoothScrollingEnabled(false); - - InfiniFrameBlazorApp app = appbuilder.Build(); - var window = app.ServiceProvider.GetRequiredService(); + string actualParameters = await RunOnStaThread(() => { + var appbuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args); + appbuilder.WindowBuilder + .SetTitle("Test") + .SetBrowserControlInitParameters(initParameters) + .SetLeft(0) + .SetTop(0) + .SetSize(100, 100) + .SetResizable(false) + .SetChromeless(true) + .SetSmoothScrollingEnabled(false); + + InfiniFrameBlazorApp app = appbuilder.Build(); + var window = app.ServiceProvider.GetRequiredService(); + string? value = window.BrowserControlInitParameters; + window.Close(); + app.DisposeAsync().AsTask().GetAwaiter().GetResult(); + return value ?? string.Empty; + }); // Assert - await Assert.That(window).IsNotNull(); - await Assert.That(window.BrowserControlInitParameters).IsEqualTo( - initParameters - ); + await Assert.That(actualParameters).IsEqualTo(initParameters); + } + + [SupportedOSPlatform("windows")] + private static Task RunOnStaThread(Func action) { + if (!OperatingSystem.IsWindows()) + throw new PlatformNotSupportedException("RunOnStaThread is only supported on Windows."); + + var source = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var thread = new Thread(() => { + try { + source.TrySetResult(action()); + } + catch (Exception ex) { + source.TrySetException(ex); + } + }) { + IsBackground = true + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return source.Task; } } From c59031646fb17afb98464f3618876dbeaf8b737a Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 6 May 2026 14:12:16 +0200 Subject: [PATCH 6/7] Chore: Add comments for imports and code sections in `InfiniFrameNativeStatusCode` enum --- .../Native/InfiniFrameNativeStatusCode.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/InfiniFrame.Shared/Native/InfiniFrameNativeStatusCode.cs b/src/InfiniFrame.Shared/Native/InfiniFrameNativeStatusCode.cs index a53c46796..9858b3ea6 100644 --- a/src/InfiniFrame.Shared/Native/InfiniFrameNativeStatusCode.cs +++ b/src/InfiniFrame.Shared/Native/InfiniFrameNativeStatusCode.cs @@ -1,5 +1,11 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- namespace InfiniFrame.Native; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- internal enum InfiniFrameNativeStatusCode : int { Success = 0, InvalidArgument = 22, From a74a313ea9ffdc938759e2cf9a1f5040ff573a50 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 6 May 2026 14:15:46 +0200 Subject: [PATCH 7/7] Add `TemporaryFilesPath` configuration to tests for consistent GUID behavior - Updated window functionality tests (`SizeTests`, `WidthTests`, `HeightTests`, `LocationTests`, `TopTests`, `LeftTests`) to set `TemporaryFilesPath` to `null`. - Prevents test failures caused by inconsistent GUID behavior during parameter setup. --- tests/InfiniFrameTests/WindowFunctionalities/HeightTests.cs | 4 +++- tests/InfiniFrameTests/WindowFunctionalities/LeftTests.cs | 4 +++- tests/InfiniFrameTests/WindowFunctionalities/LocationTests.cs | 4 +++- tests/InfiniFrameTests/WindowFunctionalities/SizeTests.cs | 4 +++- tests/InfiniFrameTests/WindowFunctionalities/TopTests.cs | 4 +++- tests/InfiniFrameTests/WindowFunctionalities/WidthTests.cs | 4 +++- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/InfiniFrameTests/WindowFunctionalities/HeightTests.cs b/tests/InfiniFrameTests/WindowFunctionalities/HeightTests.cs index a2546657e..09da96551 100644 --- a/tests/InfiniFrameTests/WindowFunctionalities/HeightTests.cs +++ b/tests/InfiniFrameTests/WindowFunctionalities/HeightTests.cs @@ -37,12 +37,14 @@ public async Task Builder_ShouldOverwriteOsDefaultSizeAndCentered() { InfiniFrameNativeParameters expectedConfigParameters = new InfiniFrameWindowNativeParameterBuilder { Height = Height, UseOsDefaultSize = false, - Centered = false + Centered = false, + TemporaryFilesPath = null // Else testing fails due to the GUID behavior }.ToNativeParameters(); // Act builder.SetUseOsDefaultSize(true); builder.SetHeight(Height); + builder.SetTemporaryFilesPath(null); // Else testing fails due to the GUID behavior // Assert await Assert.That(builder.Configuration.Height).IsEqualTo(Height); diff --git a/tests/InfiniFrameTests/WindowFunctionalities/LeftTests.cs b/tests/InfiniFrameTests/WindowFunctionalities/LeftTests.cs index b84261fe5..f33a964bb 100644 --- a/tests/InfiniFrameTests/WindowFunctionalities/LeftTests.cs +++ b/tests/InfiniFrameTests/WindowFunctionalities/LeftTests.cs @@ -36,13 +36,15 @@ public async Task Builder_ShouldOverwriteOsDefaultLocationAndCentered() { InfiniFrameNativeParameters expectedConfigParameters = new InfiniFrameWindowNativeParameterBuilder { Left = Left, UseOsDefaultLocation = false, - Centered = false + Centered = false, + TemporaryFilesPath = null // Else testing fails due to the GUID behavior }.ToNativeParameters(); // Act builder.Center(); builder.SetUseOsDefaultLocation(true); builder.SetLeft(Left); + builder.SetTemporaryFilesPath(null); // Else testing fails due to the GUID behavior // Assert await Assert.That(builder.Configuration.Left).IsEqualTo(Left); diff --git a/tests/InfiniFrameTests/WindowFunctionalities/LocationTests.cs b/tests/InfiniFrameTests/WindowFunctionalities/LocationTests.cs index d740381c0..1624b2b8b 100644 --- a/tests/InfiniFrameTests/WindowFunctionalities/LocationTests.cs +++ b/tests/InfiniFrameTests/WindowFunctionalities/LocationTests.cs @@ -42,12 +42,14 @@ public async Task Builder_ShouldOverwriteOsDefaultLocationAndCentered() { Left = Left, Top = Top, UseOsDefaultLocation = false, - Centered = false + Centered = false, + TemporaryFilesPath = null // Else testing fails due to the GUID behavior }.ToNativeParameters(); // Act builder.SetUseOsDefaultLocation(true); builder.SetLocation(Left, Top); + builder.SetTemporaryFilesPath(null); // Else testing fails due to the GUID behavior // Assert await Assert.That(builder.Configuration.Left).IsEqualTo(Left); diff --git a/tests/InfiniFrameTests/WindowFunctionalities/SizeTests.cs b/tests/InfiniFrameTests/WindowFunctionalities/SizeTests.cs index 9b9eb4d68..f2dccf45f 100644 --- a/tests/InfiniFrameTests/WindowFunctionalities/SizeTests.cs +++ b/tests/InfiniFrameTests/WindowFunctionalities/SizeTests.cs @@ -41,12 +41,14 @@ public async Task Builder_ShouldOverwriteOsDefaultSizeAndCentered() { Width = Width, Height = Height, UseOsDefaultSize = false, - Centered = false + Centered = false, + TemporaryFilesPath = null // Else testing fails due to the GUID behavior }.ToNativeParameters(); // Act builder.SetUseOsDefaultSize(true); builder.SetSize(Width, Height); + builder.SetTemporaryFilesPath(null); // Else testing fails due to the GUID behavior // Assert await Assert.That(builder.Configuration.Width).IsEqualTo(Width); diff --git a/tests/InfiniFrameTests/WindowFunctionalities/TopTests.cs b/tests/InfiniFrameTests/WindowFunctionalities/TopTests.cs index 0e2d86c40..633444bcd 100644 --- a/tests/InfiniFrameTests/WindowFunctionalities/TopTests.cs +++ b/tests/InfiniFrameTests/WindowFunctionalities/TopTests.cs @@ -36,13 +36,15 @@ public async Task Builder_ShouldOverwriteOsDefaultLocationAndCentered() { InfiniFrameNativeParameters expectedConfigParameters = new InfiniFrameWindowNativeParameterBuilder { Top = Top, UseOsDefaultLocation = false, - Centered = false + Centered = false, + TemporaryFilesPath = null // Else testing fails due to the GUID behavior }.ToNativeParameters(); // Act builder.Center(); builder.SetUseOsDefaultLocation(true); builder.SetTop(Top); + builder.SetTemporaryFilesPath(null); // Else testing fails due to the GUID behavior // Assert await Assert.That(builder.Configuration.Top).IsEqualTo(Top); diff --git a/tests/InfiniFrameTests/WindowFunctionalities/WidthTests.cs b/tests/InfiniFrameTests/WindowFunctionalities/WidthTests.cs index 779b22b89..f0edd5ad8 100644 --- a/tests/InfiniFrameTests/WindowFunctionalities/WidthTests.cs +++ b/tests/InfiniFrameTests/WindowFunctionalities/WidthTests.cs @@ -37,12 +37,14 @@ public async Task Builder_ShouldOverwriteOsDefaultSizeAndCentered() { InfiniFrameNativeParameters expectedConfigParameters = new InfiniFrameWindowNativeParameterBuilder { Width = Width, UseOsDefaultSize = false, - Centered = false + Centered = false, + TemporaryFilesPath = null // Else testing fails due to the GUID behavior }.ToNativeParameters(); // Act builder.SetUseOsDefaultSize(true); builder.SetWidth(Width); + builder.SetTemporaryFilesPath(null); // Else testing fails due to the GUID behavior // Assert await Assert.That(builder.Configuration.Width).IsEqualTo(Width);