diff --git a/.gitattributes b/.gitattributes index 10cf9919c..b7f18b462 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -src/InfiniFrame.Native/Dependencies/* linguist-vendored -src/InfiniFrame.Native/Dependencies/**/* linguist-vendored +src/InfiniFrame.NativeBridge/Native/Dependencies/* linguist-vendored +src/InfiniFrame.NativeBridge/Native/Dependencies/**/* linguist-vendored *.sh text eol=lf \ No newline at end of file diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml index 45ee89219..3bb3c721b 100644 --- a/.github/codeql-config.yml +++ b/.github/codeql-config.yml @@ -10,16 +10,16 @@ paths: # Exclude third-party and generated content from CodeQL alerting. paths-ignore: - - "src/InfiniFrame.Native/artifacts/**" - - "src/InfiniFrame.Native/build/**" - - "src/InfiniFrame.Native/packages/**" - - "src/InfiniFrame.Native/cmake-build-debug/**" - - "src/InfiniFrame.Native/cmake-build-debug-linux/**" - - "src/InfiniFrame.Native/cmake-build-debug-windows/**" - - "src/InfiniFrame.Native/cmake-build-release/**" - - "src/InfiniFrame.Native/cmake-build-release-linux/**" - - "src/InfiniFrame.Native/cmake-build-release-windows/**" - - "src/InfiniFrame.Native/Dependencies/**" + - "src/InfiniFrame.NativeBridge/artifacts/**" + - "src/InfiniFrame.NativeBridge/build/**" + - "src/InfiniFrame.NativeBridge/Native/cmake-build-debug/**" + - "src/InfiniFrame.NativeBridge/Native/cmake-build-debug-linux/**" + - "src/InfiniFrame.NativeBridge/Native/cmake-build-debug-windows/**" + - "src/InfiniFrame.NativeBridge/Native/cmake-build-release/**" + - "src/InfiniFrame.NativeBridge/Native/cmake-build-release-linux/**" + - "src/InfiniFrame.NativeBridge/Native/cmake-build-release-windows/**" + - "src/InfiniFrame.NativeBridge/Native/Dependencies/**" + - "src/InfiniFrame.NativeBridge/Native/packages/**" - "tests/**" - "**/node_modules/**" - "**/dist/**" diff --git a/.github/scripts/bump_version.py b/.github/scripts/bump_version.py index 526ae5b27..cf4e49f8b 100644 --- a/.github/scripts/bump_version.py +++ b/.github/scripts/bump_version.py @@ -11,7 +11,7 @@ # Resolve paths from the repository root: .github/scripts -> repo root is three levels up. REPO_ROOT: Final[Path] = Path(__file__).parent.parent.parent FILE: Final[Path] = REPO_ROOT / "src" / "Directory.Build.props" -CMAKE_FILE: Final[Path] = REPO_ROOT / "src" / "InfiniFrame.Native" / "CMakeLists.txt" +CMAKE_FILE: Final[Path] = REPO_ROOT / "src" / "InfiniFrame.NativeBridge" / "Native" / "CMakeLists.txt" VERSION_PATTERN: Final[re.Pattern[str]] = re.compile(r"^\d+\.\d+\.\d+(-preview\.\d+)?$") BumpPart = Literal["major", "minor", "patch", "preview"] diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index ff402c488..2464640f8 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "CodeQL Advanced" concurrency: @@ -73,7 +74,7 @@ jobs: - '.github/codeql-config.yml' cpp: - - 'src/InfiniFrame.Native/**' + - 'src/InfiniFrame.NativeBridge/Native/**' - 'native-vendor-deps.json' - 'global.json' - '.github/actions/setup-dependencies-native/**' @@ -220,11 +221,11 @@ jobs: category: "/language:csharp" analyze-cpp: - name: Analyze C/C++ (${{ matrix.os }}) + name: Analyze C/C++ (${{ matrix.os }} / ${{ matrix.arch }}) needs: changes runs-on: ${{ matrix.os }} timeout-minutes: 90 - + if: > ( github.event_name == 'workflow_dispatch' && @@ -232,15 +233,20 @@ jobs: ) || ( needs.changes.outputs.cpp == 'true' ) - + strategy: fail-fast: false matrix: - os: - - ubuntu-latest - - windows-latest - # - macos-latest # support is currently experimental and requires additional setup - + include: + - os: ubuntu-latest + arch: x64 + + - os: windows-latest + arch: x64 + +# - os: macos-latest +# arch: arm64 + permissions: contents: read security-events: write @@ -248,7 +254,7 @@ jobs: packages: read pull-requests: write checks: write - + steps: - name: Checkout uses: actions/checkout@v6 @@ -261,8 +267,8 @@ jobs: - name: Setup Native Dependencies uses: ./.github/actions/setup-dependencies-native with: - brew-cache-key: ${{ runner.os }}-brew-codeql-${{ hashFiles('.github/actions/setup-dependencies-native/action.yml', '.github/workflows/shared-testing-macos.yml') }} - brew-restore-key: ${{ runner.os }}-brew-codeql- + brew-cache-key: ${{ matrix.os }}-${{ matrix.arch }}-brew-codeql-${{ hashFiles('.github/actions/setup-dependencies-native/action.yml', '.github/workflows/ci-codeql.yml') }} + brew-restore-key: ${{ matrix.os }}-${{ matrix.arch }}-brew-codeql- - name: Initialize CodeQL uses: github/codeql-action/init@v4 @@ -277,18 +283,14 @@ jobs: dotnet restore src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj /p:NoWarn=NU1503 - name: Build NativeBridge - shell: pwsh + timeout-minutes: 45 run: | - dotnet build src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj ` - --configuration Release ` - --no-restore ` - -p:SolutionDir="${{ github.workspace }}/" ` - -p:Platform=x64 + dotnet build src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj --configuration Release --no-restore /p:SolutionDir=${{ github.workspace }}/ /p:NativeArch=${{ matrix.arch }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: - category: "/language:c-cpp" + category: "/language:c-cpp/${{ matrix.os }}-${{ matrix.arch }}" analyze-javascript-typescript: name: Analyze JavaScript / TypeScript diff --git a/.github/workflows/ci-release-docs.yml b/.github/workflows/ci-release-docs.yml index d2af5a28f..4f90bfedf 100644 --- a/.github/workflows/ci-release-docs.yml +++ b/.github/workflows/ci-release-docs.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "CI: Release Docs" on: workflow_dispatch: @@ -8,7 +9,6 @@ permissions: id-token: write jobs: - # noinspection UndefinedAction, UndefinedParamsPresent deploy-docs: name: Publish Docs uses: ./.github/workflows/shared-release-publish-docs.yml diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index abd640296..1dbc122a3 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "CI: Release" on: workflow_dispatch: @@ -35,12 +36,10 @@ permissions: id-token: write jobs: - # noinspection UndefinedAction tests-python: name: Python Tests uses: ./.github/workflows/shared-testing-python.yml - # noinspection UndefinedAction, UndefinedParamsPresent tests-platform: name: Platform Tests needs: tests-python @@ -54,12 +53,12 @@ jobs: run_macos: true run_docs: true run_trim_aot: true + enable_test_exports: false secrets: inherit # ------------------------------------------------------------------------------------------------------------------ # Bump version, commit, and tag # ------------------------------------------------------------------------------------------------------------------ - # noinspection UndefinedAction, UndefinedParamsPresent bump-version: name: Bump Version needs: tests-platform @@ -70,7 +69,6 @@ jobs: custom_version: ${{ inputs.custom_version }} secrets: inherit - # noinspection UndefinedAction, UndefinedParamsPresent build: name: Build Native needs: bump-version @@ -79,7 +77,6 @@ jobs: tag_name: ${{ needs.bump-version.outputs.tag_name }} secrets: inherit - # noinspection UndefinedAction, UndefinedParamsPresent publish: name: Publish Release needs: [ build, bump-version ] @@ -90,7 +87,6 @@ jobs: publish_to_nuget: ${{ inputs.publish_to_nuget }} secrets: inherit - # noinspection UndefinedAction, UndefinedParamsPresent publish-docs: name: Publish Docs needs: [ publish, bump-version ] diff --git a/.github/workflows/ci-testing-python.yml b/.github/workflows/ci-testing-python.yml index 6fd79653c..49bf36193 100644 --- a/.github/workflows/ci-testing-python.yml +++ b/.github/workflows/ci-testing-python.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "CI: Python Tests" on: pull_request: @@ -20,7 +21,6 @@ permissions: contents: read jobs: - # noinspection UndefinedAction run: name: Python Tests uses: ./.github/workflows/shared-testing-python.yml diff --git a/.github/workflows/ci-testing.yml b/.github/workflows/ci-testing.yml index 909605230..1ea2c7d9f 100644 --- a/.github/workflows/ci-testing.yml +++ b/.github/workflows/ci-testing.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "CI: Full Test Suite" permissions: @@ -38,12 +39,10 @@ on: default: true jobs: - # noinspection UndefinedAction python-tests: name: Python Tests uses: ./.github/workflows/shared-testing-python.yml - # noinspection UndefinedAction, UndefinedParamsPresent run: name: Full Test Suite needs: python-tests @@ -56,4 +55,5 @@ jobs: run_macos: ${{ inputs.run_macos }} run_docs: ${{ inputs.run_docs }} run_trim_aot: ${{ inputs.run_trim_aot }} + enable_test_exports: true secrets: inherit diff --git a/.github/workflows/ci-todo.yml b/.github/workflows/ci-todo.yml index f63416fa1..b35132747 100644 --- a/.github/workflows/ci-todo.yml +++ b/.github/workflows/ci-todo.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "CI: Todo Sync" on: push: diff --git a/.github/workflows/ci-update-native-vendor-deps.yml b/.github/workflows/ci-update-native-vendor-deps.yml index 446755bc0..f0eb91c4f 100644 --- a/.github/workflows/ci-update-native-vendor-deps.yml +++ b/.github/workflows/ci-update-native-vendor-deps.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "CI: Update Native Vendor Deps" on: diff --git a/.github/workflows/shared-release-build.yml b/.github/workflows/shared-release-build.yml index 3a8dbe3e1..0f2069225 100644 --- a/.github/workflows/shared-release-build.yml +++ b/.github/workflows/shared-release-build.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Release Build" on: workflow_call: @@ -57,9 +58,7 @@ jobs: 10.x - name: Setup Native dependencies - # noinspection UndefinedAction uses: ./.github/actions/setup-dependencies-native - # noinspection UndefinedParamsPresent with: brew-cache-key: ${{ matrix.os }}-${{ matrix.arch }}-brew-native-${{ hashFiles('.github/actions/setup-dependencies-native/action.yml', '.github/workflows/shared-release-build.yml') }} brew-restore-key: ${{ matrix.os }}-${{ matrix.arch }}-brew-native- diff --git a/.github/workflows/shared-release-bump-version.yml b/.github/workflows/shared-release-bump-version.yml index 9c322cd7b..52c0b5d51 100644 --- a/.github/workflows/shared-release-bump-version.yml +++ b/.github/workflows/shared-release-bump-version.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Release Bump Version" on: workflow_call: diff --git a/.github/workflows/shared-release-publish-docs.yml b/.github/workflows/shared-release-publish-docs.yml index 494bd81d0..904385cb1 100644 --- a/.github/workflows/shared-release-publish-docs.yml +++ b/.github/workflows/shared-release-publish-docs.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Release Docs" on: workflow_call: diff --git a/.github/workflows/shared-release-publish.yml b/.github/workflows/shared-release-publish.yml index 061dc29fe..98869e080 100644 --- a/.github/workflows/shared-release-publish.yml +++ b/.github/workflows/shared-release-publish.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Release Publish" on: workflow_call: @@ -50,9 +51,7 @@ jobs: --prefix InfiniLore - name: Setup Native dependencies - # noinspection UndefinedAction uses: ./.github/actions/setup-dependencies-native - # noinspection UndefinedParamsPresent with: brew-cache-key: ${{ matrix.os }}-${{ matrix.arch }}-brew-native-${{ hashFiles('.github/actions/setup-dependencies-native/action.yml', '.github/workflows/shared-release-publish.yml') }} brew-restore-key: ${{ matrix.os }}-${{ matrix.arch }}-brew-native- diff --git a/.github/workflows/shared-testing-build.yml b/.github/workflows/shared-testing-build.yml index a5735591c..10bcc7472 100644 --- a/.github/workflows/shared-testing-build.yml +++ b/.github/workflows/shared-testing-build.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Testing Build" on: workflow_call: @@ -15,6 +16,11 @@ on: description: 'Resolved commit SHA used by testing workflows.' type: string required: true + enable_test_exports: + description: 'Enable native test exports in testing native artifacts.' + type: boolean + required: false + default: false permissions: contents: read @@ -68,7 +74,8 @@ jobs: run: | ./src/InfiniFrame.NativeBridge/native-build.ps1 ` "Release" ` - "${{ matrix.arch }}" + "${{ matrix.arch }}" ` + "${{ inputs.enable_test_exports }}" - name: Upload Build Artifact uses: actions/upload-artifact@v7 diff --git a/.github/workflows/shared-testing-docs.yml b/.github/workflows/shared-testing-docs.yml index 8585f0d97..b1c783ead 100644 --- a/.github/workflows/shared-testing-docs.yml +++ b/.github/workflows/shared-testing-docs.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Docs Tests" on: @@ -46,9 +47,7 @@ jobs: - name: Set Docs Pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction, UndefinedParamsPresent uses: ./.github/actions/sync-check - # noinspection UndefinedAction, UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ env.TARGET_SHA }} @@ -66,9 +65,7 @@ jobs: if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction, UndefinedParamsPresent uses: ./.github/actions/sync-check-finalize - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ env.TARGET_SHA }} diff --git a/.github/workflows/shared-testing-dotnetpack.yml b/.github/workflows/shared-testing-dotnetpack.yml index feaa5112c..7a11048c2 100644 --- a/.github/workflows/shared-testing-dotnetpack.yml +++ b/.github/workflows/shared-testing-dotnetpack.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Dotnet Pack Tests" on: @@ -37,12 +38,13 @@ jobs: with: cache-dotnet: false + - name: Setup dependencies + uses: ./.github/actions/setup-dependencies-native + - name: Set Dotnet Pack Check Pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} @@ -66,14 +68,16 @@ jobs: --configuration Release \ --no-restore \ /p:SolutionDir=${{ github.workspace }}/ \ - /p:InfiniFrameSkipNativeBuild=true + /p:InfiniFrameSkipNativeBuild=false \ + /p:InfiniFrameEnableTestExports=false - name: Pack run: | dotnet pack InfiniFrame.GitHubActions.Release.slnf \ --configuration Release \ --no-build \ - /p:InfiniFrameSkipNativeBuild=true \ + /p:InfiniFrameSkipNativeBuild=false \ + /p:InfiniFrameEnableTestExports=false \ -o ./release - name: Verify NativeBridge Package Natives @@ -112,6 +116,59 @@ jobs: echo "❌ $MISSING native file(s) missing in NativeBridge package" exit 1 fi + + - name: Verify Lack of NativeBridge Test Exports + shell: bash + run: | + set -euo pipefail + + PACKAGE=$(ls ./release/InfiniLore.InfiniFrame.NativeBridge.*.nupkg 2>/dev/null | grep -v '\.symbols\.nupkg$' | head -n 1 || true) + + if [ -z "$PACKAGE" ]; then + echo "❌ No InfiniFrame.NativeBridge package found in ./release" + exit 1 + fi + + echo "Inspecting package: $PACKAGE" + + DLL_PATH="runtimes/win-x64/native/InfiniFrame.Native.dll" + + if ! unzip -l "$PACKAGE" | grep -q "$DLL_PATH"; then + echo "❌ Missing $DLL_PATH in package" + exit 1 + fi + + mkdir -p ./tmp/native-check + + unzip -p "$PACKAGE" "$DLL_PATH" > ./tmp/native-check/InfiniFrame.Native.dll + + sudo apt-get update + sudo apt-get install -y binutils-mingw-w64-x86-64 + + DLL=./tmp/native-check/InfiniFrame.Native.dll + + FORBIDDEN_EXPORTS=( + "InfiniFrameNativeTests_NativeParametersReturnAsIs" + "InfiniFrameNativeTests_FreeInitParams" + ) + + FOUND=0 + + for export_name in "${FORBIDDEN_EXPORTS[@]}"; do + if x86_64-w64-mingw32-nm -g --defined-only "$DLL" | awk '{print $3}' | grep -Fxq "$export_name"; then + echo "❌ Forbidden test export found: $export_name" + FOUND=$((FOUND+1)) + else + echo "✅ Export not present: $export_name" + fi + done + + if [ "$FOUND" -gt 0 ]; then + echo "❌ $FOUND forbidden test export(s) found in InfiniFrame.Native.dll" + exit 1 + fi + + echo "✅ No forbidden test exports found" - name: Complete Dotnet Pack Check if: always() diff --git a/.github/workflows/shared-testing-js.yml b/.github/workflows/shared-testing-js.yml index 22134c3aa..e0c051bc3 100644 --- a/.github/workflows/shared-testing-js.yml +++ b/.github/workflows/shared-testing-js.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Js Tests" on: @@ -55,9 +56,7 @@ jobs: if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction, UndefinedParamsPresent uses: ./.github/actions/sync-check-finalize - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ env.TARGET_SHA }} diff --git a/.github/workflows/shared-testing-linux.yml b/.github/workflows/shared-testing-linux.yml index 4fab3a858..49d9eeab5 100644 --- a/.github/workflows/shared-testing-linux.yml +++ b/.github/workflows/shared-testing-linux.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Linux Tests" on: workflow_call: @@ -15,6 +16,10 @@ on: workflow_url: type: string required: true + enable_test_exports: + type: boolean + required: false + default: false jobs: linux: @@ -56,9 +61,7 @@ jobs: - name: Set Linux Check Pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} @@ -77,7 +80,7 @@ jobs: run: sudo glib-compile-schemas /usr/share/glib-2.0/schemas/ - name: Restore - run: dotnet restore InfiniFrame.GitHubActions.Testing.slnf /p:NoWarn=NU1503 + run: dotnet restore InfiniFrame.GitHubActions.Testing.slnf /p:NoWarn=NU1503 /p:NativeArch=${{ matrix.arch }} - name: Build Release run: | @@ -85,8 +88,9 @@ jobs: --configuration Release \ --no-restore \ -p:SolutionDir=${{ github.workspace }}/ \ - -p:CMakePlatform=${{ matrix.arch }} \ - -p:InfiniFrameSkipNativeBuild=true + -p:NativeArch=${{ matrix.arch }} \ + -p:InfiniFrameSkipNativeBuild=true \ + -p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Export Actions Runtime uses: actions/github-script@v9 @@ -168,13 +172,12 @@ jobs: --configuration Release \ --no-build \ --no-restore \ - -p:CMakePlatform=${{ matrix.arch }} \ - -p:InfiniFrameSkipNativeBuild=true + -p:NativeArch=${{ matrix.arch }} \ + -p:InfiniFrameSkipNativeBuild=true \ + -p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Pack Tool E2E - # noinspection UndefinedAction uses: ./.github/actions/packtool-e2e - # noinspection UndefinedParamsPresent with: project: examples/InfiniFrameExample.SingleFileExe/InfiniFrameExample.SingleFileExe.csproj rid: ${{ matrix.rid }} @@ -191,9 +194,7 @@ jobs: if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction, UndefinedParamsPresent uses: ./.github/actions/sync-check-finalize - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} diff --git a/.github/workflows/shared-testing-macos.yml b/.github/workflows/shared-testing-macos.yml index 8c2db0dd1..cfd954481 100644 --- a/.github/workflows/shared-testing-macos.yml +++ b/.github/workflows/shared-testing-macos.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: macOS Tests" on: workflow_call: @@ -15,6 +16,10 @@ on: workflow_url: type: string required: true + enable_test_exports: + type: boolean + required: false + default: false jobs: macos: @@ -61,9 +66,7 @@ jobs: - name: Set macOS Check Pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} @@ -79,7 +82,7 @@ jobs: artifact-type: testing - name: Restore - run: dotnet restore InfiniFrame.GitHubActions.Testing.slnf /p:NoWarn=NU1503 + run: dotnet restore InfiniFrame.GitHubActions.Testing.slnf /p:NoWarn=NU1503 /p:NativeArch=${{ matrix.arch }} - name: Build Release run: | @@ -87,8 +90,9 @@ jobs: --configuration Release \ --no-restore \ -p:SolutionDir=${{ github.workspace }}/ \ - -p:CMakePlatform=${{ matrix.arch }} \ - -p:InfiniFrameSkipNativeBuild=true + -p:NativeArch=${{ matrix.arch }} \ + -p:InfiniFrameSkipNativeBuild=true \ + -p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Export Actions Runtime uses: actions/github-script@v9 @@ -115,13 +119,12 @@ jobs: --configuration Release \ --no-build \ --no-restore \ - -p:CMakePlatform=${{ matrix.arch }} \ - -p:InfiniFrameSkipNativeBuild=true + -p:NativeArch=${{ matrix.arch }} \ + -p:InfiniFrameSkipNativeBuild=true \ + -p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Pack Tool E2E - # noinspection UndefinedAction uses: ./.github/actions/packtool-e2e - # noinspection UndefinedParamsPresent with: project: examples/InfiniFrameExample.SingleFileExe/InfiniFrameExample.SingleFileExe.csproj rid: ${{ matrix.rid }} @@ -138,9 +141,7 @@ jobs: if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check-finalize - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} diff --git a/.github/workflows/shared-testing-python.yml b/.github/workflows/shared-testing-python.yml index 8dcc64b4b..5af6b9803 100644 --- a/.github/workflows/shared-testing-python.yml +++ b/.github/workflows/shared-testing-python.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Python Tests" permissions: contents: read diff --git a/.github/workflows/shared-testing-windows-playwright.yml b/.github/workflows/shared-testing-windows-playwright.yml index 97b457aa0..8f205f680 100644 --- a/.github/workflows/shared-testing-windows-playwright.yml +++ b/.github/workflows/shared-testing-windows-playwright.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Windows Playwright Tests" on: @@ -16,6 +17,10 @@ on: workflow_url: type: string required: true + enable_test_exports: + type: boolean + required: false + default: false permissions: contents: read @@ -38,9 +43,7 @@ jobs: - name: Set Playwright Check Pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} @@ -98,9 +101,7 @@ jobs: - name: Set Playwright Check Pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} @@ -146,7 +147,8 @@ jobs: --configuration Release ` --no-restore ` /p:SolutionDir=$env:GITHUB_WORKSPACE/ ` - /p:InfiniFrameSkipNativeBuild=true + /p:InfiniFrameSkipNativeBuild=true ` + /p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Install Playwright Browsers shell: pwsh @@ -187,7 +189,8 @@ jobs: --no-build ` --no-restore ` --framework net8.0 ` - /p:InfiniFrameSkipNativeBuild=true + /p:InfiniFrameSkipNativeBuild=true ` + /p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Run Playwright tests NET 9.0 shell: pwsh @@ -198,7 +201,8 @@ jobs: --no-build ` --no-restore ` --framework net9.0 ` - /p:InfiniFrameSkipNativeBuild=true + /p:InfiniFrameSkipNativeBuild=true ` + /p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Run Playwright tests NET 10.0 shell: pwsh @@ -209,15 +213,14 @@ jobs: --no-build ` --no-restore ` --framework net10.0 ` - /p:InfiniFrameSkipNativeBuild=true + /p:InfiniFrameSkipNativeBuild=true ` + /p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Update Playwright Check if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check-finalize - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} @@ -245,9 +248,7 @@ jobs: - name: Set Playwright Check Final Status env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check-finalize - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} diff --git a/.github/workflows/shared-testing-windows-trim-aot.yml b/.github/workflows/shared-testing-windows-trim-aot.yml index d102b3ff4..5f6823dfd 100644 --- a/.github/workflows/shared-testing-windows-trim-aot.yml +++ b/.github/workflows/shared-testing-windows-trim-aot.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Windows Trim/AOT Validation" on: @@ -12,6 +13,11 @@ on: description: 'Commit SHA used for status/check updates' type: string required: true + enable_test_exports: + description: 'Enable native/managed test exports in trim/AOT validation builds.' + type: boolean + required: false + default: false permissions: contents: read @@ -42,9 +48,7 @@ jobs: - name: Set Trim/AOT Check Pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction, UndefinedParamsPresent uses: ./.github/actions/sync-check - # noinspection UndefinedAction, UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ env.TARGET_SHA }} @@ -55,9 +59,7 @@ jobs: allow-status-422: 'true' - name: Setup Native dependencies - # noinspection UndefinedAction uses: ./.github/actions/setup-dependencies-native - # noinspection UndefinedParamsPresent with: brew-cache-key: ${{ runner.os }}-x64-brew-native-${{ hashFiles('.github/actions/setup-dependencies-native/action.yml', '.github/workflows/shared-testing-windows-trim-aot.yml') }} brew-restore-key: ${{ runner.os }}-x64-brew-native- @@ -75,6 +77,7 @@ jobs: -p:SolutionDir=$env:GITHUB_WORKSPACE/ ` -p:Platform=x64 ` -p:InfiniFrameSkipNativeBuild=true ` + -p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} ` -p:EnableTrimAnalyzer=true ` -p:EnableAotAnalyzer=true ` -p:GeneratePackageOnBuild=false @@ -85,6 +88,7 @@ jobs: -p:SolutionDir=$env:GITHUB_WORKSPACE/ ` -p:Platform=x64 ` -p:InfiniFrameSkipNativeBuild=true ` + -p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} ` -p:EnableTrimAnalyzer=true ` -p:EnableAotAnalyzer=true ` -p:GeneratePackageOnBuild=false ` @@ -96,6 +100,7 @@ jobs: -p:SolutionDir=$env:GITHUB_WORKSPACE/ ` -p:Platform=x64 ` -p:InfiniFrameSkipNativeBuild=true ` + -p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} ` -p:EnableTrimAnalyzer=true ` -p:EnableAotAnalyzer=true ` -p:GeneratePackageOnBuild=false ` @@ -110,6 +115,7 @@ jobs: -p:SolutionDir=$env:GITHUB_WORKSPACE/ ` -p:Platform=x64 ` -p:InfiniFrameSkipNativeBuild=true ` + -p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} ` -p:GeneratePackageOnBuild=false ` -p:SkipTypeScriptBuild=true @@ -145,9 +151,7 @@ jobs: if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction, UndefinedParamsPresent uses: ./.github/actions/sync-check-finalize - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ env.TARGET_SHA }} diff --git a/.github/workflows/shared-testing-windows.yml b/.github/workflows/shared-testing-windows.yml index 36dca79f8..17da2199a 100644 --- a/.github/workflows/shared-testing-windows.yml +++ b/.github/workflows/shared-testing-windows.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Windows Tests" on: workflow_call: @@ -15,6 +16,10 @@ on: workflow_url: type: string required: true + enable_test_exports: + type: boolean + required: false + default: false jobs: windows: @@ -58,9 +63,7 @@ jobs: - name: Set Windows Check Pending env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction uses: ./.github/actions/sync-check - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} @@ -76,7 +79,7 @@ jobs: artifact-type: testing - name: Restore - run: dotnet restore InfiniFrame.GitHubActions.Testing.slnf /p:NoWarn=NU1503 + run: dotnet restore InfiniFrame.GitHubActions.Testing.slnf /p:NoWarn=NU1503 /p:NativeArch=${{ matrix.arch }} - name: Build Release shell: pwsh @@ -85,8 +88,9 @@ jobs: --configuration Release ` --no-restore ` /p:SolutionDir=$env:GITHUB_WORKSPACE/ ` - /p:CMakePlatform=${{ matrix.arch }} ` - /p:InfiniFrameSkipNativeBuild=true + /p:NativeArch=${{ matrix.arch }} ` + /p:InfiniFrameSkipNativeBuild=true ` + /p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} - name: Prepare ARM64 Crash Diagnostics if: matrix.arch == 'arm64' @@ -199,8 +203,9 @@ jobs: --configuration Release ` --no-build ` --no-restore ` - /p:CMakePlatform=${{ matrix.arch }} ` - /p:InfiniFrameSkipNativeBuild=true + /p:NativeArch=${{ matrix.arch }} ` + /p:InfiniFrameSkipNativeBuild=true ` + /p:InfiniFrameEnableTestExports=${{ inputs.enable_test_exports }} # - name: Upload ARM64 Crash Diagnostics # if: always() && matrix.arch == 'arm64' @@ -215,9 +220,7 @@ jobs: # artifacts/testresults/** - name: Pack Tool E2E - # noinspection UndefinedAction uses: ./.github/actions/packtool-e2e - # noinspection UndefinedParamsPresent with: project: examples/InfiniFrameExample.SingleFileExe/InfiniFrameExample.SingleFileExe.csproj rid: ${{ matrix.rid }} @@ -234,9 +237,7 @@ jobs: if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # noinspection UndefinedAction, UndefinedParamsPresent uses: ./.github/actions/sync-check-finalize - # noinspection UndefinedParamsPresent with: repo: ${{ github.repository }} sha: ${{ inputs.target_sha }} diff --git a/.github/workflows/shared-testing.yml b/.github/workflows/shared-testing.yml index dd0627efa..3cc819fdd 100644 --- a/.github/workflows/shared-testing.yml +++ b/.github/workflows/shared-testing.yml @@ -1,3 +1,4 @@ +#file: noinspection UndefinedAction,UndefinedParamsPresent name: "Shared: Platform Tests" permissions: contents: read @@ -39,6 +40,11 @@ on: description: 'Run trim and NativeAOT compatibility validation' type: boolean required: true + enable_test_exports: + description: 'Enable native/managed test exports for test workflows' + type: boolean + required: false + default: false jobs: @@ -71,7 +77,6 @@ jobs: echo "sha=$SHA" >> $GITHUB_OUTPUT - # noinspection UndefinedAction, UndefinedParamsPresent docs-validation: name: Validate Docs if: ${{ inputs.run_docs == true }} @@ -82,7 +87,6 @@ jobs: target_sha: ${{ needs.prepare.outputs.sha }} secrets: inherit - # noinspection UndefinedAction, UndefinedParamsPresent js-validation: name: Validate JS needs: [ prepare ] @@ -91,7 +95,6 @@ jobs: checkout_ref: ${{ inputs.checkout_ref }} target_sha: ${{ needs.prepare.outputs.sha }} - # noinspection UndefinedAction, UndefinedParamsPresent native-build: name: Build Native Artifacts needs: [ prepare ] @@ -100,8 +103,8 @@ jobs: pr_number: ${{ inputs.pr_number }} checkout_ref: ${{ inputs.checkout_ref }} target_sha: ${{ needs.prepare.outputs.sha }} + enable_test_exports: ${{ inputs.enable_test_exports }} - # noinspection UndefinedAction, UndefinedParamsPresent dotnetpack-validation: name: Validate Dotnet Pack needs: [ prepare, native-build ] @@ -110,7 +113,6 @@ jobs: checkout_ref: ${{ inputs.checkout_ref }} target_sha: ${{ needs.prepare.outputs.sha }} - # noinspection UndefinedAction, UndefinedParamsPresent linux: name: Linux Tests if: ${{ inputs.run_linux == true }} @@ -121,9 +123,9 @@ jobs: checkout_ref: ${{ inputs.checkout_ref }} target_sha: ${{ needs.prepare.outputs.sha }} workflow_url: ${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }} + enable_test_exports: ${{ inputs.enable_test_exports }} secrets: inherit - # noinspection UndefinedAction, UndefinedParamsPresent macos: name: macOS Tests if: ${{ inputs.run_macos == true }} @@ -134,9 +136,9 @@ jobs: checkout_ref: ${{ inputs.checkout_ref }} target_sha: ${{ needs.prepare.outputs.sha }} workflow_url: ${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }} + enable_test_exports: ${{ inputs.enable_test_exports }} secrets: inherit - # noinspection UndefinedAction, UndefinedParamsPresent windows: name: Windows Tests if: ${{ inputs.run_windows == true }} @@ -147,9 +149,9 @@ jobs: checkout_ref: ${{ inputs.checkout_ref }} target_sha: ${{ needs.prepare.outputs.sha }} workflow_url: ${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }} + enable_test_exports: ${{ inputs.enable_test_exports }} secrets: inherit - # noinspection UndefinedAction, UndefinedParamsPresent windows-playwright: name: Windows Playwright Tests if: ${{ inputs.run_windows_playwright == true }} @@ -160,9 +162,9 @@ jobs: checkout_ref: ${{ inputs.checkout_ref }} target_sha: ${{ needs.prepare.outputs.sha }} workflow_url: ${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }} + enable_test_exports: ${{ inputs.enable_test_exports }} secrets: inherit - # noinspection UndefinedAction, UndefinedParamsPresent windows-trim-aot: name: Validate Trim/AOT if: ${{ inputs.run_trim_aot == true }} @@ -171,3 +173,4 @@ jobs: with: checkout_ref: ${{ inputs.checkout_ref }} target_sha: ${{ needs.prepare.outputs.sha }} + enable_test_exports: ${{ inputs.enable_test_exports }} diff --git a/.gitignore b/.gitignore index 6845edc86..8a1c6c100 100644 --- a/.gitignore +++ b/.gitignore @@ -346,11 +346,16 @@ healthchecksdb /src/InfiniFrame.NativeBridge/artifacts/ /src/InfiniFrame.NativeBridge/build/ /src/InfiniFrame.NativeBridge/Native/build/ +/src/InfiniFrame.NativeBridge/Native/build-clang-tidy/ /src/InfiniFrame.NativeBridge/Native/packages/ /src/InfiniFrame.NativeBridge/Native/cmake-build-debug/ /src/InfiniFrame.NativeBridge/Native/cmake-build-debug-linux/ /src/InfiniFrame.NativeBridge/Native/cmake-build-debug-windows/ +/src/InfiniFrame.NativeBridge/Native/cmake-build-release/ +/src/InfiniFrame.NativeBridge/Native/cmake-build-release-linux/ +/src/InfiniFrame.NativeBridge/Native/cmake-build-release-windows/ /src/InfiniFrame.NativeBridge/Native/Embedded/InfiniFrameJs/InfiniFrameJs.cpp +/src/InfiniFrame.NativeBridge/Native/Embedded/InfiniFrameJs/InfiniFrameJs.h # wwwroot folders from js web based projects /examples/InfiniFrameExample.WebApp.React/wwwroot/ diff --git a/Directory.Packages.props b/Directory.Packages.props index 08a46edde..2307816fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,9 +33,9 @@ - - - - + + + + \ No newline at end of file diff --git a/InfiniFrame.slnx b/InfiniFrame.slnx index 85f479354..1d28fd2f5 100644 --- a/InfiniFrame.slnx +++ b/InfiniFrame.slnx @@ -177,7 +177,6 @@ - diff --git a/assets/icon.png b/assets/icon.png index 0d00ee25f..ad89505d6 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props index 0e955e684..18a8d8e74 100644 --- a/examples/Directory.Build.props +++ b/examples/Directory.Build.props @@ -2,10 +2,10 @@ Exe - + net10.0 14.0 - + enable enable false diff --git a/examples/InfiniFrameExample.BlazorWebView/Components/App.razor b/examples/InfiniFrameExample.BlazorWebView/Components/App.razor index 1ad1dd6ec..7978ff4ce 100644 --- a/examples/InfiniFrameExample.BlazorWebView/Components/App.razor +++ b/examples/InfiniFrameExample.BlazorWebView/Components/App.razor @@ -1,6 +1,5 @@ @using InfiniFrameExample.BlazorWebView.Components.Layouts @using InfiniFrameExample.BlazorWebView.Components.Pages - diff --git a/examples/InfiniFrameExample.BlazorWebView/Components/Pages/PageNotFound.razor b/examples/InfiniFrameExample.BlazorWebView/Components/Pages/PageNotFound.razor index 2d1913c9b..20b1013c5 100644 --- a/examples/InfiniFrameExample.BlazorWebView/Components/Pages/PageNotFound.razor +++ b/examples/InfiniFrameExample.BlazorWebView/Components/Pages/PageNotFound.razor @@ -1,12 +1,12 @@ @* ------------------------------------------------------------------------------------------------------------------ *@ @* Imports @* ------------------------------------------------------------------------------------------------------------------ *@ -@using InfiniFrameExample.BlazorWebView.Components.Layouts @* ------------------------------------------------------------------------------------------------------------------ *@ @* Descriptors @* ------------------------------------------------------------------------------------------------------------------ *@ @page "/PageNotFound" +@using InfiniFrameExample.BlazorWebView.Components.Layouts @layout MainLayout @* ------------------------------------------------------------------------------------------------------------------ *@ @@ -19,6 +19,6 @@ @* ------------------------------------------------------------------------------------------------------------------ *@ @code { - + } diff --git a/examples/InfiniFrameExample.BlazorWebView/InfiniFrameExample.BlazorWebView.csproj b/examples/InfiniFrameExample.BlazorWebView/InfiniFrameExample.BlazorWebView.csproj index 96eda0933..32916d77a 100644 --- a/examples/InfiniFrameExample.BlazorWebView/InfiniFrameExample.BlazorWebView.csproj +++ b/examples/InfiniFrameExample.BlazorWebView/InfiniFrameExample.BlazorWebView.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/examples/InfiniFrameExample.BlazorWebView/README.md b/examples/InfiniFrameExample.BlazorWebView/README.md index 92baad31b..e7b4277e6 100644 --- a/examples/InfiniFrameExample.BlazorWebView/README.md +++ b/examples/InfiniFrameExample.BlazorWebView/README.md @@ -1,6 +1,7 @@ # Example: BlazorWebView -Demonstrates the minimal setup for hosting a Blazor application inside a native InfiniFrame window using `InfiniLore.InfiniFrame.BlazorWebView` +Demonstrates the minimal setup for hosting a Blazor application inside a native InfiniFrame window using +`InfiniLore.InfiniFrame.BlazorWebView` ## What it shows diff --git a/examples/InfiniFrameExample.BlazorWebView/wwwroot/sample-data/weather.json b/examples/InfiniFrameExample.BlazorWebView/wwwroot/sample-data/weather.json index 06463c02f..bed52c923 100644 --- a/examples/InfiniFrameExample.BlazorWebView/wwwroot/sample-data/weather.json +++ b/examples/InfiniFrameExample.BlazorWebView/wwwroot/sample-data/weather.json @@ -1,27 +1,27 @@ [ - { - "date": "2018-05-06", - "temperatureC": 1, - "summary": "Freezing" - }, - { - "date": "2018-05-07", - "temperatureC": 14, - "summary": "Bracing" - }, - { - "date": "2018-05-08", - "temperatureC": -13, - "summary": "Freezing" - }, - { - "date": "2018-05-09", - "temperatureC": -16, - "summary": "Balmy" - }, - { - "date": "2018-05-10", - "temperatureC": -2, - "summary": "Chilly" - } + { + "date": "2018-05-06", + "temperatureC": 1, + "summary": "Freezing" + }, + { + "date": "2018-05-07", + "temperatureC": 14, + "summary": "Bracing" + }, + { + "date": "2018-05-08", + "temperatureC": -13, + "summary": "Freezing" + }, + { + "date": "2018-05-09", + "temperatureC": -16, + "summary": "Balmy" + }, + { + "date": "2018-05-10", + "temperatureC": -2, + "summary": "Chilly" + } ] diff --git a/scripts/BuildFrontend.mjs b/scripts/BuildFrontend.mjs index 9e7207a31..d30625e6c 100644 --- a/scripts/BuildFrontend.mjs +++ b/scripts/BuildFrontend.mjs @@ -45,6 +45,22 @@ function isProcessRunning(processId) { function shouldRemoveExistingLock() { const ownerFile = path.join(lockDirectory, 'owner.txt'); const staleLockThresholdMilliseconds = 10 * 60 * 1000; + const lockAgeMilliseconds = (() => { + try { + return Date.now() - statSync(lockDirectory).mtimeMs; + } catch (error) { + if (error?.code === 'ENOENT') { + return null; + } + + throw error; + } + })(); + + // PIDs can be recycled by the OS. If the lock is old, remove it regardless of PID state. + if (lockAgeMilliseconds !== null && lockAgeMilliseconds > staleLockThresholdMilliseconds) { + return true; + } if (existsSync(ownerFile)) { const ownerProcessId = Number.parseInt(readFileSync(ownerFile, 'utf8'), 10); @@ -56,17 +72,7 @@ function shouldRemoveExistingLock() { return false; } } - - try { - const lockAgeMilliseconds = Date.now() - statSync(lockDirectory).mtimeMs; - return lockAgeMilliseconds > staleLockThresholdMilliseconds; - } catch (error) { - if (error?.code === 'ENOENT') { - return false; - } - - throw error; - } + return false; } function acquireLock() { diff --git a/scripts/clean.ps1 b/scripts/clean.ps1 index b15dede17..292fdef01 100644 --- a/scripts/clean.ps1 +++ b/scripts/clean.ps1 @@ -10,4 +10,25 @@ Get-ChildItem -Path $Root -Directory -Recurse | Remove-Item $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } -Write-Host "Done cleaning bin/obj folders." \ No newline at end of file +# Additional cleanup paths +$ExtraPaths = @( + "../src/InfiniFrame.NativeBridge/build", + "../src/InfiniFrame.NativeBridge/artifacts", + "../src/InfiniFrame.Js/node_modules", + "../src/InfiniFrame.NativeBridge/Native/cmake-build-debug-linux", + "../src/InfiniFrame.NativeBridge/Native/cmake-build-debug-windows", + "../src/InfiniFrame.NativeBridge/Native/cmake-build-release-linux", + "../src/InfiniFrame.NativeBridge/Native/cmake-build-release-windows", + "../src/InfiniFrame.NativeBridge/Native/packages" +) + +foreach ($RelativePath in $ExtraPaths) { + $FullPath = Join-Path $Root $RelativePath + + if (Test-Path $FullPath) { + Write-Host "Deleting $FullPath" + Remove-Item $FullPath -Recurse -Force -ErrorAction SilentlyContinue + } +} + +Write-Host "Done cleaning bin/obj folders and extra build artifacts." \ No newline at end of file diff --git a/scripts/clion-linux-environment.sh b/scripts/clion-linux-environment.sh index 20f3bba93..d4584728e 100644 --- a/scripts/clion-linux-environment.sh +++ b/scripts/clion-linux-environment.sh @@ -11,10 +11,24 @@ sudo apt install -y \ gnupg \ software-properties-common \ wget \ + curl \ build-essential \ pkg-config \ lsb-release +# ---------------------------------------------------------------------------------------------------------------------- +# Node.js 24 +# ---------------------------------------------------------------------------------------------------------------------- +echo "Installing Node.js 24..." + +curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - + +sudo apt install -y nodejs + +echo "Node version:" +node --version +npm --version + # ---------------------------------------------------------------------------------------------------------------------- # CMake (latest via Kitware) # ---------------------------------------------------------------------------------------------------------------------- @@ -77,6 +91,21 @@ echo "Installing Clang toolchain (optional but recommended)..." sudo apt install -y clang libc++-dev libc++abi-dev || true +# ---------------------------------------------------------------------------------------------------------------------- +# GDB / WSL Debugging Support +# ---------------------------------------------------------------------------------------------------------------------- +echo "Installing GDB debugger support..." + +sudo apt install -y \ + gdb \ + gdbserver + +echo "GDB version:" +gdb --version || true + +echo "GDB path:" +which gdb || true + # ---------------------------------------------------------------------------------------------------------------------- # GTK / WebKit / Native deps # ---------------------------------------------------------------------------------------------------------------------- @@ -106,6 +135,12 @@ echo "Verifying toolchain..." echo "CMake version:" cmake --version +echo "Node version:" +node --version + +echo "NPM version:" +npm --version + echo "GCC version:" gcc --version || true @@ -115,5 +150,12 @@ g++ --version || true echo "Clang version:" clang++ --version || true +echo "GDB version:" +gdb --version || true + +echo "" +echo "Setup complete!" + echo "" -echo "Setup complete!" \ No newline at end of file +echo "Recommended CLion debugger path:" +echo "/usr/bin/gdb" \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 61a0873c2..e8c3d3eaf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -32,8 +32,7 @@ true true - false - + true 0.13.1 InfiniFrame, TryPhotino diff --git a/src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj b/src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj index 0423a2968..17a71ac77 100644 --- a/src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj +++ b/src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj @@ -3,11 +3,15 @@ InfiniLore.InfiniFrame.NativeBridge Library + true true - x64 - $(Platform) + x64 + x64 + x64 + arm64 + arm64 windows linux @@ -18,6 +22,8 @@ $(MSBuildThisFileDirectory)artifacts\native false + true + false $(NativeOutputRoot)\windows\x64\$(Configuration) $(NativeOutputRoot)\windows\arm64\$(Configuration) @@ -27,6 +33,10 @@ $(NativeOutputRoot)\osx\arm64\$(Configuration) + + $(DefineConstants);InfiniFrameNativeTestExports + + @@ -48,77 +58,101 @@ - - + + false + InfiniFrame.Native.dll + runtimes/win-x64/native/ + PreserveNewest + PreserveNewest + true + - + + false + WebView2Loader.dll + runtimes/win-x64/native/ + PreserveNewest + PreserveNewest + true + - + + false + InfiniFrame.Native.dll + runtimes/win-arm64/native/ + PreserveNewest + PreserveNewest + true + - + + false + WebView2Loader.dll + runtimes/win-arm64/native/ + PreserveNewest + PreserveNewest + true + - + + false + InfiniFrame.Native.so + runtimes/linux-x64/native/ + PreserveNewest + PreserveNewest + true + - + + false + InfiniFrame.Native.so + runtimes/linux-arm64/native/ + PreserveNewest + PreserveNewest + true + - + + false + InfiniFrame.Native.dylib + runtimes/osx-x64/native/ + PreserveNewest + PreserveNewest + true + - + + false + InfiniFrame.Native.dylib + runtimes/osx-arm64/native/ + PreserveNewest + PreserveNewest + true + @@ -127,16 +161,16 @@ - + - + @@ -147,28 +181,28 @@ Text="✅ Found InfiniFrame.Native.dll in $(WindowsX64)" Importance="High"/> @@ -177,28 +211,28 @@ Text="✅ Found InfiniFrame.Native.dll in $(WindowsArm64)" Importance="High"/> diff --git a/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNative.cs b/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNative.cs index e6e3732c5..f3fae3bab 100644 --- a/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNative.cs +++ b/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNative.cs @@ -18,226 +18,229 @@ public static partial class InfiniFrameNative { #region MARSHAL CALLS FROM Non-UI Thread to UI Thread [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_Invoke", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Invoke(IntPtr instance, Action callback); + internal static partial InfiniFrameNativeInteropStatus Invoke(IntPtr instance, Action callback); #endregion #region Register // ReSharper disable once UnusedMethodReturnValue.Local [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_register_win32", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void RegisterWin32(IntPtr hInstance); + internal static partial InfiniFrameNativeInteropStatus RegisterWin32(IntPtr hInstance); // ReSharper disable once UnusedMethodReturnValue.Local [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_register_mac", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void RegisterMac(); + internal static partial InfiniFrameNativeInteropStatus RegisterMac(); #endregion #region CTOR-DTOR [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_ctor", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial IntPtr Constructor([MarshalUsing(typeof(InfiniFrameNativeParametersMarshaller))] in InfiniFrameNativeParameters parameters); + internal static partial InfiniFrameNativeInteropStatus Constructor([MarshalUsing(typeof(InfiniFrameNativeParametersMarshaller))] in InfiniFrameNativeParameters parameters, out IntPtr value); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_dtor"), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Destructor(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus Destructor(IntPtr instance); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_AddCustomSchemeName", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void AddCustomSchemeName(IntPtr instance, string scheme); + internal static partial InfiniFrameNativeInteropStatus AddCustomSchemeName(IntPtr instance, string scheme); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_Close", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Close(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus Close(IntPtr instance); #endregion #region Get [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_getHwnd_win32", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial IntPtr GetWindowHandlerWin32(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus GetWindowHandlerWin32(IntPtr instance, out IntPtr value); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_GetAllMonitors", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetAllMonitors(IntPtr instance, CppGetAllMonitorsDelegate callback); + internal static partial InfiniFrameNativeInteropStatus GetAllMonitors(IntPtr instance, CppGetAllMonitorsDelegate callback); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetTransparentEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetContextMenuEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetDevToolsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetFullScreen(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool fullScreen); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetGrantBrowserPermissions(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool grant); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_GetUserAgent", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial IntPtr GetUserAgent(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus GetUserAgent(IntPtr instance, out IntPtr value); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetMediaAutoplayEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetFileSystemAccessEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetWebSecurityEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetJavascriptClipboardAccessEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetMediaStreamEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetSmoothScrollingEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetIgnoreCertificateErrorsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetNotificationsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool enabled); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetPosition(IntPtr instance, out int x, out int y); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetResizable(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool resizable); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_GetScreenDpi", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial uint GetScreenDpi(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus GetScreenDpi(IntPtr instance, out uint value); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetSize(IntPtr instance, out int width, out int height); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetMaxSize(IntPtr instance, out int maxWidth, out int maxHeight); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetMinSize(IntPtr instance, out int minWidth, out int minHeight); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_GetTitle", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial IntPtr GetTitle(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus GetTitle(IntPtr instance, out IntPtr value); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetTopmost(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool topmost); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_GetZoom", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void GetZoom(IntPtr instance, out int zoom); + internal static partial InfiniFrameNativeInteropStatus GetZoom(IntPtr instance, out int zoom); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetMaximized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool maximized); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetMinimized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool minimized); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetZoomEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool zoomEnabled); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_GetIconFileName", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial IntPtr GetIconFileName(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus GetIconFileName(IntPtr instance, out IntPtr value); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus GetFocused(IntPtr instance, [MarshalAs(UnmanagedType.I1)] out bool isFocused); #endregion #region Navigate [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_NavigateToString", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void NavigateToString(IntPtr instance, string content); + internal static partial InfiniFrameNativeInteropStatus NavigateToString(IntPtr instance, string content); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_NavigateToUrl", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void NavigateToUrl(IntPtr instance, string url); + internal static partial InfiniFrameNativeInteropStatus NavigateToUrl(IntPtr instance, string url); #endregion #region Set [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus SetWebView2RuntimePath_win32(IntPtr instance, string webView2RuntimePath); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetTransparentEnabled", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetTransparentEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); + internal static partial InfiniFrameNativeInteropStatus SetTransparentEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetContextMenuEnabled", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetContextMenuEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); + internal static partial InfiniFrameNativeInteropStatus SetContextMenuEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetDevToolsEnabled", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetDevToolsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); + internal static partial InfiniFrameNativeInteropStatus SetDevToolsEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetFullScreen", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetFullScreen(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool fullScreen); + internal static partial InfiniFrameNativeInteropStatus SetFullScreen(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool fullScreen); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetMaximized", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetMaximized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool maximized); + internal static partial InfiniFrameNativeInteropStatus SetMaximized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool maximized); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetMaxSize", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetMaxSize(IntPtr instance, int maxWidth, int maxHeight); + internal static partial InfiniFrameNativeInteropStatus SetMaxSize(IntPtr instance, int maxWidth, int maxHeight); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetMinimized", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetMinimized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool minimized); + internal static partial InfiniFrameNativeInteropStatus SetMinimized(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool minimized); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetMinSize", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetMinSize(IntPtr instance, int minWidth, int minHeight); + internal static partial InfiniFrameNativeInteropStatus SetMinSize(IntPtr instance, int minWidth, int minHeight); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetResizable", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetResizable(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool resizable); + internal static partial InfiniFrameNativeInteropStatus SetResizable(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool resizable); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetPosition", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetPosition(IntPtr instance, int x, int y); + internal static partial InfiniFrameNativeInteropStatus SetPosition(IntPtr instance, int x, int y); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetSize", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetSize(IntPtr instance, int width, int height); + internal static partial InfiniFrameNativeInteropStatus SetSize(IntPtr instance, int width, int height); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetTitle", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetTitle(IntPtr instance, string? title); + internal static partial InfiniFrameNativeInteropStatus SetTitle(IntPtr instance, string? title); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetTopmost", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetTopmost(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool topmost); + internal static partial InfiniFrameNativeInteropStatus SetTopmost(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool topmost); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetIconFile", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetIconFile(IntPtr instance, string filename); + internal static partial InfiniFrameNativeInteropStatus SetIconFile(IntPtr instance, string filename); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetZoom", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetZoom(IntPtr instance, int zoom); + internal static partial InfiniFrameNativeInteropStatus SetZoom(IntPtr instance, int zoom); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetZoomEnabled", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetZoomEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool zoomEnabled); + internal static partial InfiniFrameNativeInteropStatus SetZoomEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool zoomEnabled); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SetFocused", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SetFocused(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus SetFocused(IntPtr instance); #endregion #region Misc [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_Center", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Center(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus Center(IntPtr instance); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_Restore", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void Restore(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus Restore(IntPtr instance); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_ClearBrowserAutoFill", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void ClearBrowserAutoFill(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus ClearBrowserAutoFill(IntPtr instance); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_SendWebMessage", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void SendWebMessage(IntPtr instance, string message); + internal static partial InfiniFrameNativeInteropStatus SendWebMessage(IntPtr instance, string message); [LibraryImport(NativeLibraryName, 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 InfiniFrameNativeInteropStatus ShowNotification(IntPtr instance, string title, string body); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_WaitForExit", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void WaitForExit(IntPtr instance); + internal static partial InfiniFrameNativeInteropStatus WaitForExit(IntPtr instance); #endregion #region Dialog [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_ShowOpenFile", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial IntPtr ShowOpenFile(IntPtr inst, string title, string defaultPath, [MarshalAs(UnmanagedType.I1)] bool multiSelect, string[] filters, int filtersCount, out int resultCount); + internal static partial InfiniFrameNativeInteropStatus ShowOpenFile(IntPtr inst, string title, string defaultPath, [MarshalAs(UnmanagedType.I1)] bool multiSelect, string[] filters, int filtersCount, out int resultCount, out IntPtr values); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_ShowOpenFolder", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial IntPtr ShowOpenFolder(IntPtr inst, string title, string defaultPath, [MarshalAs(UnmanagedType.I1)] bool multiSelect, out int resultCount); + internal static partial InfiniFrameNativeInteropStatus ShowOpenFolder(IntPtr inst, string title, string defaultPath, [MarshalAs(UnmanagedType.I1)] bool multiSelect, out int resultCount, out IntPtr values); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_ShowSaveFile", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial IntPtr ShowSaveFile(IntPtr inst, string title, string defaultPath, string[] filters, int filtersCount, string? defaultFileName); + internal static partial InfiniFrameNativeInteropStatus ShowSaveFile(IntPtr inst, string title, string defaultPath, string[] filters, int filtersCount, string? defaultFileName, out IntPtr value); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_ShowMessage", SetLastError = true, StringMarshalling = StringMarshalling.Utf8), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial InfiniFrameDialogResult ShowMessage(IntPtr inst, string title, string text, InfiniFrameDialogButtons buttons, InfiniFrameDialogIcon icon); + internal static partial InfiniFrameNativeInteropStatus ShowMessage(IntPtr inst, string title, string text, InfiniFrameDialogButtons buttons, InfiniFrameDialogIcon icon, out InfiniFrameDialogResult value); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_FreeString", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void FreeString(IntPtr value); + internal static partial InfiniFrameNativeInteropStatus FreeString(IntPtr value); [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_FreeStringArray", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial void FreeStringArray(IntPtr values, int count); + internal static partial InfiniFrameNativeInteropStatus FreeStringArray(IntPtr values, int count); + + [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrame_GetLastErrorMessage", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial InfiniFrameNativeInteropStatus GetLastErrorMessagePtr(out IntPtr value); #endregion #region Overloads @@ -249,59 +252,147 @@ 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 InfiniFrameNativeInteropStatus GetHeight(IntPtr instance, out int height) + => GetSize(instance, out _, out height); + + internal static InfiniFrameNativeInteropStatus GetWidth(IntPtr instance, out int width) + => GetSize(instance, out width, out _); + + internal static InfiniFrameNativeInteropStatus GetMaxHeight(IntPtr instance, out int maxHeight) + => GetMaxSize(instance, out _, out maxHeight); + + internal static InfiniFrameNativeInteropStatus GetMaxWidth(IntPtr instance, out int maxWidth) + => GetMaxSize(instance, out maxWidth, out _); + + internal static InfiniFrameNativeInteropStatus GetMinHeight(IntPtr instance, out int minHeight) + => GetMinSize(instance, out _, out minHeight); + + internal static InfiniFrameNativeInteropStatus GetMinWidth(IntPtr instance, out int minWidth) + => GetMinSize(instance, out minWidth, out _); + + internal static InfiniFrameNativeInteropStatus GetLeft(IntPtr instance, out int left) + => GetPosition(instance, out left, out _); - 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); + internal static InfiniFrameNativeInteropStatus GetTop(IntPtr instance, out int top) + => GetPosition(instance, out _, out top); - internal static void GetSize(IntPtr instance, out Size size) { - GetSize(instance, out int width, out int height); + internal static InfiniFrameNativeInteropStatus GetSize(IntPtr instance, out Size size) { + InfiniFrameNativeInteropStatus 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 InfiniFrameNativeInteropStatus GetMaxSize(IntPtr instance, out Size size) { + InfiniFrameNativeInteropStatus 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 InfiniFrameNativeInteropStatus GetMinSize(IntPtr instance, out Size size) { + InfiniFrameNativeInteropStatus 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 InfiniFrameNativeInteropStatus GetPosition(IntPtr instance, out Point position) { + InfiniFrameNativeInteropStatus 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 InfiniFrameNativeInteropStatus GetWindowRectangle(IntPtr instance, out int x, out int y, out int width, out int height) { + InfiniFrameNativeInteropStatus sizeStatus = GetSize(instance, out width, out height); + if (sizeStatus != InfiniFrameNativeInteropStatus.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 InfiniFrameNativeInteropStatus GetWindowRectangle(IntPtr instance, out Rectangle rectangle) { + InfiniFrameNativeInteropStatus 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 InfiniFrameNativeInteropStatus GetUserAgent(IntPtr instance, out string? userAgent) { + InfiniFrameNativeInteropStatus status = GetUserAgent(instance, out IntPtr ptr); + try { + userAgent = PtrToNativeString(ptr); + } + finally { + if (ptr != IntPtr.Zero) { + FreeString(ptr); + } + } + + return status; } - internal static void GetUserAgent(IntPtr instance, out string? userAgent) { - IntPtr ptr = GetUserAgent(instance); - userAgent = PtrToNativeString(ptr); + internal static InfiniFrameNativeInteropStatus GetTitle(IntPtr instance, out string? title) { + InfiniFrameNativeInteropStatus status = GetTitle(instance, out IntPtr ptr); + try { + title = PtrToNativeString(ptr); + } + finally { + if (ptr != IntPtr.Zero) { + FreeString(ptr); + } + } + + return status; } - internal static void GetTitle(IntPtr instance, out string? title) { - IntPtr ptr = GetTitle(instance); - title = PtrToNativeString(ptr); + internal static InfiniFrameNativeInteropStatus GetIconFileName(IntPtr instance, out string iconFileName) { + InfiniFrameNativeInteropStatus status = GetIconFileName(instance, out IntPtr ptr); + try { + iconFileName = PtrToNativeString(ptr) ?? string.Empty; + } + finally { + if (ptr != IntPtr.Zero) { + FreeString(ptr); + } + } + + return status; + } + + internal static string? GetLastErrorMessage() { + InfiniFrameNativeInteropStatus status = GetLastErrorMessagePtr(out IntPtr ptr); + if (status != InfiniFrameNativeInteropStatus.Success || ptr == IntPtr.Zero) return null; + + try { + return PtrToNativeString(ptr); + } + finally { + FreeString(ptr); + } } - internal static void GetIconFileName(IntPtr instance, out string iconFileName) { - IntPtr ptr = GetIconFileName(instance); - iconFileName = PtrToNativeString(ptr) ?? string.Empty; + internal static InfiniFrameNativeInteropStatus EnsureSucceeded(InfiniFrameNativeInteropStatus status, string operationName) { + + int fallbackLastError = Marshal.GetLastPInvokeError(); + + if (status is InfiniFrameNativeInteropStatus.Success && fallbackLastError is 0) return status; + + InfiniFrameNativeInteropStatus fallbackStatus = GetLastErrorMessagePtr(out IntPtr ptr); + + string? fallbackMessage; + if (fallbackStatus != InfiniFrameNativeInteropStatus.Success || ptr == IntPtr.Zero) { + fallbackMessage = "No native error message provided."; + } + else { + try { + fallbackMessage = PtrToNativeString(ptr); + } + finally { + FreeString(ptr); + } + } + + throw new ApplicationException($"Native interop call '{operationName}' failed with unknown status state. Fallback last error {fallbackLastError}. {fallbackMessage} {fallbackStatus}"); } #endregion } diff --git a/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNativeInteropStatus.cs b/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNativeInteropStatus.cs new file mode 100644 index 000000000..d86e6c874 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNativeInteropStatus.cs @@ -0,0 +1,13 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace InfiniFrame.NativeBridge; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +internal enum InfiniFrameNativeInteropStatus { + Success = 0, + InvalidArgument = 22, + OutParameterSetToInvalidNull = 2001, + OperationFailed = 14 +} diff --git a/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNativeTesting.cs b/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNativeTesting.cs index 1eb408204..cb545f853 100644 --- a/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNativeTesting.cs +++ b/src/InfiniFrame.NativeBridge/Managed/LibraryImports/InfiniFrameNativeTesting.cs @@ -12,22 +12,25 @@ namespace InfiniFrame.NativeBridge; // Code // --------------------------------------------------------------------------------------------------------------------- public static partial class InfiniFrameNativeTesting { - [LibraryImport(NativeLibraryName, EntryPoint = "InfiniWindowTests_NativeParametersReturnAsIs", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - private static partial void NativeParametersReturnAsIsNative( +// #if InfiniFrameNativeTestExports + [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrameNativeTests_NativeParametersReturnAsIs", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial InfiniFrameNativeInteropStatus NativeParametersReturnAsIsNative( [MarshalUsing(typeof(InfiniFrameNativeParametersMarshaller))] in InfiniFrameNativeParameters parameters, out IntPtr newParameters ); - [LibraryImport(NativeLibraryName, EntryPoint = "InfiniWindowTests_FreeInitParams", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - private static partial void FreeInitParamsNative(IntPtr parameters); + [LibraryImport(NativeLibraryName, EntryPoint = "InfiniFrameNativeTests_FreeInitParams", SetLastError = true), UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial InfiniFrameNativeInteropStatus FreeInitParamsNative(IntPtr parameters); /// /// Returns a native pointer to a newly allocated InfiniFrameInitParams clone. /// Ownership is transferred to managed caller, which must call exactly once. /// internal static IntPtr NativeParametersReturnAsIsPtr(ref InfiniFrameNativeParameters parameters) { - NativeParametersReturnAsIsNative(in parameters, out IntPtr newParametersPtr); + InfiniFrameNative.EnsureSucceeded( + NativeParametersReturnAsIsNative(in parameters, out IntPtr newParametersPtr), + nameof(NativeParametersReturnAsIsNative)); // ReSharper disable once ConvertIfStatementToReturnStatement if (newParametersPtr == IntPtr.Zero) throw new InvalidOperationException("Native function returned null pointer"); @@ -38,6 +41,19 @@ internal static IntPtr NativeParametersReturnAsIsPtr(ref InfiniFrameNativeParame internal static void FreeInitParams(IntPtr newParametersPtr) { if (newParametersPtr == IntPtr.Zero) return; - FreeInitParamsNative(newParametersPtr); + InfiniFrameNative.EnsureSucceeded( + FreeInitParamsNative(newParametersPtr), + nameof(FreeInitParamsNative)); } +// #else +// internal static IntPtr NativeParametersReturnAsIsPtr(ref InfiniFrameNativeParameters parameters) { +// throw new PlatformNotSupportedException("InfiniFrame native test exports are not enabled for this build."); +// } +// +// internal static void FreeInitParams(IntPtr newParametersPtr) { +// if (newParametersPtr != IntPtr.Zero) { +// throw new PlatformNotSupportedException("InfiniFrame native test exports are not enabled for this build."); +// } +// } +// #endif } diff --git a/src/InfiniFrame.NativeBridge/Native/.clang-format b/src/InfiniFrame.NativeBridge/Native/.clang-format new file mode 100644 index 000000000..da19dd245 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/.clang-format @@ -0,0 +1,119 @@ +BasedOnStyle: Microsoft +Language: Cpp + +# ---------------------------------------------------------------------------------------------------------------------- +# General Style +# ---------------------------------------------------------------------------------------------------------------------- + +IndentWidth: 4 +TabWidth: 4 +UseTab: Never + +ColumnLimit: 120 +MaxEmptyLinesToKeep: 1 + +DerivePointerAlignment: false +PointerAlignment: Left +ReferenceAlignment: Left + +NamespaceIndentation: All + +SortIncludes: false +SortUsingDeclarations: false + +# ---------------------------------------------------------------------------------------------------------------------- +# Braces / K&R Style +# ---------------------------------------------------------------------------------------------------------------------- + +BreakBeforeBraces: Attach + +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false + +# ---------------------------------------------------------------------------------------------------------------------- +# Short Statements +# ---------------------------------------------------------------------------------------------------------------------- + +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortEnumsOnASingleLine: false + +# ---------------------------------------------------------------------------------------------------------------------- +# Templates +# ---------------------------------------------------------------------------------------------------------------------- + +AlwaysBreakTemplateDeclarations: MultiLine + +# ---------------------------------------------------------------------------------------------------------------------- +# Spacing +# ---------------------------------------------------------------------------------------------------------------------- + +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesInParentheses: false +SpacesInAngles: Never +SpacesInContainerLiterals: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false + +# ---------------------------------------------------------------------------------------------------------------------- +# Alignment +# ---------------------------------------------------------------------------------------------------------------------- + +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignOperands: DontAlign +AlignTrailingComments: false + +# ---------------------------------------------------------------------------------------------------------------------- +# Access Modifiers +# ---------------------------------------------------------------------------------------------------------------------- + +IndentAccessModifiers: false +AccessModifierOffset: 0 + +# ---------------------------------------------------------------------------------------------------------------------- +# Includes +# ---------------------------------------------------------------------------------------------------------------------- + +IncludeBlocks: Preserve + +# ---------------------------------------------------------------------------------------------------------------------- +# Line Breaking +# ---------------------------------------------------------------------------------------------------------------------- + +AlignAfterOpenBracket: BlockIndent +BinPackParameters: false +BreakConstructorInitializers: BeforeComma +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 + +# ---------------------------------------------------------------------------------------------------------------------- +# C++11/20 Friendly +# ---------------------------------------------------------------------------------------------------------------------- + +Standard: Latest +Cpp11BracedListStyle: true + +# ---------------------------------------------------------------------------------------------------------------------- +# Misc +# ---------------------------------------------------------------------------------------------------------------------- + +IndentCaseLabels: true +KeepEmptyLinesAtTheStartOfBlocks: false +ReflowComments: false diff --git a/src/InfiniFrame.NativeBridge/Native/.clang-tidy b/src/InfiniFrame.NativeBridge/Native/.clang-tidy index e7949f724..351dac4af 100644 --- a/src/InfiniFrame.NativeBridge/Native/.clang-tidy +++ b/src/InfiniFrame.NativeBridge/Native/.clang-tidy @@ -1,146 +1,100 @@ -# Generated from CLion Inspection settings ---- -Checks: '-*, -bugprone-argument-comment, -bugprone-assert-side-effect, -bugprone-bad-signal-to-kill-thread, -bugprone-branch-clone, -bugprone-copy-constructor-init, -bugprone-dangling-handle, -bugprone-dynamic-static-initializers, -bugprone-fold-init-type, -bugprone-forward-declaration-namespace, -bugprone-forwarding-reference-overload, -bugprone-inaccurate-erase, -bugprone-incorrect-roundings, -bugprone-integer-division, -bugprone-lambda-function-name, -bugprone-macro-parentheses, -bugprone-macro-repeated-side-effects, -bugprone-misplaced-operator-in-strlen-in-alloc, -bugprone-misplaced-pointer-arithmetic-in-alloc, -bugprone-misplaced-widening-cast, -bugprone-move-forwarding-reference, -bugprone-multiple-statement-macro, -bugprone-no-escape, -bugprone-parent-virtual-call, -bugprone-posix-return, -bugprone-reserved-identifier, -bugprone-sizeof-container, -bugprone-sizeof-expression, -bugprone-spuriously-wake-up-functions, -bugprone-string-constructor, -bugprone-string-integer-assignment, -bugprone-string-literal-with-embedded-nul, -bugprone-suspicious-enum-usage, -bugprone-suspicious-include, -bugprone-suspicious-memset-usage, -bugprone-suspicious-missing-comma, -bugprone-suspicious-semicolon, -bugprone-suspicious-string-compare, -bugprone-suspicious-memory-comparison, -bugprone-suspicious-realloc-usage, -bugprone-swapped-arguments, -bugprone-terminating-continue, -bugprone-throw-keyword-missing, -bugprone-too-small-loop-variable, -bugprone-undefined-memory-manipulation, -bugprone-undelegated-constructor, -bugprone-unhandled-self-assignment, -bugprone-unused-raii, -bugprone-unused-return-value, -bugprone-use-after-move, -bugprone-virtual-near-miss, -cert-dcl21-cpp, -cert-dcl58-cpp, -cert-err34-c, -cert-err52-cpp, -cert-err60-cpp, -cert-flp30-c, -cert-msc50-cpp, -cert-msc51-cpp, -cert-str34-c, -cppcoreguidelines-interfaces-global-init, -cppcoreguidelines-narrowing-conversions, -cppcoreguidelines-pro-type-member-init, -cppcoreguidelines-pro-type-static-cast-downcast, -cppcoreguidelines-slicing, -google-default-arguments, -google-explicit-constructor, -google-runtime-operator, -hicpp-exception-baseclass, -hicpp-multiway-paths-covered, -misc-misplaced-const, -misc-new-delete-overloads, -misc-non-copyable-objects, -misc-throw-by-value-catch-by-reference, -misc-unconventional-assign-operator, -misc-uniqueptr-reset-release, -modernize-avoid-bind, -modernize-concat-nested-namespaces, -modernize-deprecated-headers, -modernize-deprecated-ios-base-aliases, -modernize-loop-convert, -modernize-make-shared, -modernize-make-unique, -modernize-pass-by-value, -modernize-raw-string-literal, -modernize-redundant-void-arg, -modernize-replace-auto-ptr, -modernize-replace-disallow-copy-and-assign-macro, -modernize-replace-random-shuffle, -modernize-return-braced-init-list, -modernize-shrink-to-fit, -modernize-unary-static-assert, -modernize-use-auto, -modernize-use-bool-literals, -modernize-use-emplace, -modernize-use-equals-default, -modernize-use-equals-delete, -modernize-use-nodiscard, -modernize-use-noexcept, -modernize-use-nullptr, -modernize-use-override, -modernize-use-transparent-functors, -modernize-use-uncaught-exceptions, -mpi-buffer-deref, -mpi-type-mismatch, -openmp-use-default-none, -performance-faster-string-find, -performance-for-range-copy, -performance-implicit-conversion-in-loop, -performance-inefficient-algorithm, -performance-inefficient-string-concatenation, -performance-inefficient-vector-operation, -performance-move-const-arg, -performance-move-constructor-init, -performance-no-automatic-move, -performance-noexcept-move-constructor, -performance-trivially-destructible, -performance-type-promotion-in-math-fn, -performance-unnecessary-copy-initialization, -performance-unnecessary-value-param, -portability-simd-intrinsics, -readability-avoid-const-params-in-decls, -readability-const-return-type, -readability-container-size-empty, -readability-convert-member-functions-to-static, -readability-delete-null-pointer, -readability-deleted-default, -readability-inconsistent-declaration-parameter-name, -readability-make-member-function-const, -readability-misleading-indentation, -readability-misplaced-array-index, -readability-non-const-parameter, -readability-redundant-control-flow, -readability-redundant-declaration, -readability-redundant-function-ptr-dereference, -readability-redundant-smartptr-get, -readability-redundant-string-cstr, -readability-redundant-string-init, -readability-simplify-subscript-expr, -readability-static-accessed-through-instance, -readability-static-definition-in-anonymous-namespace, -readability-string-compare, -readability-uniqueptr-delete-release, -readability-use-anyofallof' \ No newline at end of file +Checks: > + -*, + bugprone-*, + cppcoreguidelines-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-owning-memory, + -hicpp-*, + +WarningsAsErrors: '' + +HeaderFilterRegex: '.*' + +FormatStyle: file + +CheckOptions: + + # -------------------------------------------------------------------------------------------------------------------- + # Naming Rules (C#-Style) + # -------------------------------------------------------------------------------------------------------------------- + + - key: readability-identifier-naming.ClassCase + value: CamelCase + + - key: readability-identifier-naming.StructCase + value: CamelCase + + - key: readability-identifier-naming.EnumCase + value: CamelCase + + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + + - key: readability-identifier-naming.FunctionCase + value: CamelCase + + - key: readability-identifier-naming.MethodCase + value: CamelCase + + - key: readability-identifier-naming.NamespaceCase + value: CamelCase + + - key: readability-identifier-naming.VariableCase + value: camelBack + + - key: readability-identifier-naming.ParameterCase + value: camelBack + + - key: readability-identifier-naming.MemberCase + value: CamelCase + + - key: readability-identifier-naming.PrivateMemberPrefix + value: _ + + - key: readability-identifier-naming.PrivateMemberCase + value: camelBack + + - key: readability-identifier-naming.ProtectedMemberPrefix + value: _ + + - key: readability-identifier-naming.ProtectedMemberCase + value: camelBack + + - key: readability-identifier-naming.ConstantCase + value: CamelCase + + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + + # -------------------------------------------------------------------------------------------------------------------- + # Modernization + # -------------------------------------------------------------------------------------------------------------------- + + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + + - key: modernize-loop-convert.MinConfidence + value: reasonable + + # -------------------------------------------------------------------------------------------------------------------- + # Readability + # -------------------------------------------------------------------------------------------------------------------- + + - key: readability-function-cognitive-complexity.Threshold + value: '25' + + - key: readability-function-size.LineThreshold + value: '300' + + # -------------------------------------------------------------------------------------------------------------------- + # Performance + # -------------------------------------------------------------------------------------------------------------------- + + - key: performance-move-const-arg.CheckTriviallyCopyableMove + value: 'false' diff --git a/src/InfiniFrame.NativeBridge/Native/.cmake/Embed.InfiniFrameJs.Impl.cmake b/src/InfiniFrame.NativeBridge/Native/.cmake/Embed.InfiniFrameJs.Impl.cmake index 0ea2fbf01..c8dbf964f 100644 --- a/src/InfiniFrame.NativeBridge/Native/.cmake/Embed.InfiniFrameJs.Impl.cmake +++ b/src/InfiniFrame.NativeBridge/Native/.cmake/Embed.InfiniFrameJs.Impl.cmake @@ -19,23 +19,28 @@ endforeach() string(TIMESTAMP GENERATED_AT "%Y-%m-%d %H:%M:%S UTC" UTC) # Header file -file(WRITE "${OUTPUT_HEADER}" "#pragma once +file(WRITE "${OUTPUT_HEADER}" "// ----------------------------------------------------------------------------- +// Auto-generated file. Do not edit manually. +// Generated at: ${GENERATED_AT} +// ----------------------------------------------------------------------------- +#pragma once + // ReSharper disable once CppUnusedIncludeDirective #include -extern const unsigned char g_infiniframe_js_data[]; -extern const size_t g_infiniframe_js_size; +extern const unsigned char GInfiniframeJsData[]; // NOLINT(*-avoid-c-arrays) +extern const size_t GInfiniframeJsSize; ") # Source file -file(WRITE "${OUTPUT_SOURCE}" "#include \"InfiniFrameJs.h\" - -// ----------------------------------------------------------------------------- +file(WRITE "${OUTPUT_SOURCE}" "// ----------------------------------------------------------------------------- // Auto-generated file. Do not edit manually. // Generated at: ${GENERATED_AT} // ----------------------------------------------------------------------------- -alignas(16) const unsigned char g_infiniframe_js_data[] = {${BYTES}}; +#include \"Embedded/InfiniFrameJs/InfiniFrameJs.h\" + +alignas(16) const unsigned char GInfiniframeJsData[] = {${BYTES}}; -const size_t g_infiniframe_js_size = sizeof(g_infiniframe_js_data); +const size_t GInfiniframeJsSize = sizeof(GInfiniframeJsData); ") \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/.cmake/Platform.MacOS.cmake b/src/InfiniFrame.NativeBridge/Native/.cmake/Platform.MacOS.cmake index 8be141301..90f94f39d 100644 --- a/src/InfiniFrame.NativeBridge/Native/.cmake/Platform.MacOS.cmake +++ b/src/InfiniFrame.NativeBridge/Native/.cmake/Platform.MacOS.cmake @@ -1,26 +1,29 @@ # 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) add_library(${target_name} SHARED - ${CMAKE_CURRENT_BINARY_DIR}/Exports.mm - ${CMAKE_CURRENT_BINARY_DIR}/Exports.Tests.mm + ${common_sources} + ${test_sources} ${mac_sources} ${header_files} ) + # Export units include platform headers that require Objective-C++ on macOS. + set_source_files_properties(${common_sources} ${test_sources} PROPERTIES + LANGUAGE OBJCXX + ) + target_include_directories(${target_name} PRIVATE "${CMAKE_SOURCE_DIR}") set_target_properties(${target_name} PROPERTIES PREFIX "" OUTPUT_NAME "InfiniFrame.Native" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" - OSX_ARCHITECTURES "x86_64;arm64" ) target_link_libraries(${target_name} PRIVATE @@ -34,7 +37,10 @@ function(infiniframe_configure_macos_target target_name mac_sources header_files target_compile_options(${target_name} PRIVATE -Wall -Wextra - -O2 + $<$:-O0 -g> + $<$:-O2> + $<$:-O2 -g> + $<$:-Os> -fPIC ) endfunction() diff --git a/src/InfiniFrame.NativeBridge/Native/.idea/cmake.xml b/src/InfiniFrame.NativeBridge/Native/.idea/cmake.xml new file mode 100644 index 000000000..6b28f1cd6 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/.idea/cmake.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/CMakeLists.txt b/src/InfiniFrame.NativeBridge/Native/CMakeLists.txt index bf0790ce3..b3ffb5c9e 100644 --- a/src/InfiniFrame.NativeBridge/Native/CMakeLists.txt +++ b/src/InfiniFrame.NativeBridge/Native/CMakeLists.txt @@ -1,20 +1,23 @@ -cmake_minimum_required(VERSION 4.0) +cmake_minimum_required(VERSION 4.0) project(InfiniFrame.Native VERSION 1.1.1 LANGUAGES CXX DESCRIPTION "InfiniFrame.Native webview-wrapper library" ) +if (APPLE) + enable_language(OBJCXX) +endif () + set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_OBJCXX_STANDARD 23) +set(CMAKE_OBJCXX_STANDARD_REQUIRED ON) +set(CMAKE_OBJCXX_EXTENSIONS OFF) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") -if (APPLE) - set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "" FORCE) -endif () - # ---------------------------------------------------------------------------------------------------------------------- # Dependencies # ---------------------------------------------------------------------------------------------------------------------- @@ -31,21 +34,50 @@ infiniframe_setup_dependencies() # Source Files # ---------------------------------------------------------------------------------------------------------------------- set(COMMON_SOURCES - Exports.cpp + Public/Exports/Exports.Platform.cpp + Public/Exports/Exports.Lifecycle.cpp + Public/Exports/Exports.WindowState.cpp + Public/Exports/Exports.WindowCommands.cpp + Public/Exports/Exports.Dialog.cpp + Public/Exports/Exports.Events.cpp + Public/Exports/Exports.Memory.cpp ) set(TEST_SOURCES - Exports.Tests.cpp + Public/Exports/Exports.Tests.cpp ) set(WINDOWS_SOURCES - Platform/Windows/Window.cpp + Platform/Windows/Core/WindowCore.Win32.cpp + Platform/Windows/Core/WindowTracing.Win32.cpp + Platform/Windows/Core/WindowEncoding.Win32.cpp + Platform/Windows/Core/WindowStorage.Win32.cpp + Platform/Windows/Core/WindowOwnership.Win32.cpp + Platform/Windows/Core/WindowLifecycle.Win32.cpp + Platform/Windows/Core/WindowProc.Win32.cpp + Platform/Windows/Core/WindowState.Win32.cpp + Platform/Windows/Core/WindowEvents.Win32.cpp + Platform/Windows/WebView/WebView2Host.Win32.cpp + Platform/Windows/WebView/WebView2Runtime.Win32.cpp + Platform/Windows/WebView/WebView2Controller.Win32.cpp + Platform/Windows/WebView/WebView2Attach.Win32.cpp + Platform/Windows/Core/UiDispatcher.Win32.cpp Platform/Windows/DarkMode.cpp Platform/Windows/Dialog.cpp ) set(LINUX_SOURCES - Platform/Linux/Window.cpp + Platform/Linux/Core/WindowCore.Gtk.cpp + Platform/Linux/Core/WindowInitialization.Gtk.cpp + Platform/Linux/Core/WindowLifecycle.Gtk.cpp + Platform/Linux/Core/WindowState.Gtk.cpp + Platform/Linux/Core/WindowEvents.Gtk.cpp + Platform/Linux/Core/UiDispatcher.Gtk.cpp + Platform/Linux/WebKit/WebKitHost.Gtk.cpp + Platform/Linux/WebKit/WebKitSettings.Gtk.cpp + Platform/Linux/WebKit/WebKitMessaging.Gtk.cpp + Platform/Linux/WebKit/WebKitCustomSchemes.Gtk.cpp + Platform/Linux/Core/WindowSignals.Gtk.cpp Platform/Linux/Dialog.cpp ) @@ -57,22 +89,42 @@ set(MAC_SOURCES Platform/Mac/UrlSchemeHandler.mm Platform/Mac/NSWindowBorderless.mm Platform/Mac/Dialog.mm - Platform/Mac/Window.mm + Platform/Mac/Core/WindowCore.Cocoa.mm + Platform/Mac/Core/WindowLifecycle.Cocoa.mm + Platform/Mac/Core/WindowState.Cocoa.mm + Platform/Mac/Core/WindowEvents.Cocoa.mm + Platform/Mac/Core/UiDispatcher.Cocoa.mm + Platform/Mac/WebKit/WebKitBridge.Cocoa.mm + Platform/Mac/WebKit/WebKitCustomSchemes.Cocoa.mm + Platform/Mac/WebKit/WebKitHost.Cocoa.mm ) set(HEADER_FILES - Core/InfiniFrame.h - Core/InfiniFrameWindow.h - Core/InfiniFrameDialog.h + Public/InfiniFrame.h + Public/InfiniFrameWindow.h + Public/InfiniFrameDialog.h Embedded/Embedded.h Embedded/InfiniFrameJs/InfiniFrameJs.h Types/Basic.h Types/Dialog.h - Core/InfiniFrameInitParams.h + Types/DialogResult.h + Types/DialogButtons.h + Types/DialogIcon.h + Types/Monitor.h + Public/InfiniFrameInitParams.h Types/Callbacks.h Utils/Common.h + Utils/Dimensions.h + Utils/ErrorCode.h + Utils/Result.h + Utils/StringCopy.h + Utils/WindowsHandles.h Utils/Event.h + Public/Exports/Exports.h + Utils/ExportGuards.h Platform/Windows/ToastHandler.h + Platform/Windows/Window.Win32.Internal.h + Platform/Windows/Window.Win32.Context.h Platform/Windows/DarkMode.h Platform/Mac/AppDelegate.h Platform/Mac/NavigationDelegate.h @@ -80,6 +132,9 @@ set(HEADER_FILES Platform/Mac/UiDelegate.h Platform/Mac/WindowDelegate.h Platform/Mac/UrlSchemeHandler.h + Platform/Linux/Window.Gtk.Internal.h + Platform/Linux/WebKit/WebKit.Gtk.Internal.h + Platform/Mac/Window.Cocoa.Internal.h ) if (WIN32) @@ -96,6 +151,8 @@ if (WIN32) elseif (APPLE) infiniframe_configure_macos_target( ${PROJECT_NAME} + "${COMMON_SOURCES}" + "${TEST_SOURCES}" "${MAC_SOURCES}" "${HEADER_FILES}" ) @@ -113,21 +170,29 @@ elseif (UNIX) infiniframe_setup_embed_js(${PROJECT_NAME}) endif () +if (DEFINED INFINIFRAME_BUILD_TEST_EXPORTS) + target_compile_definitions(${PROJECT_NAME} PRIVATE + $<$:INFINIFRAME_BUILD_TEST_EXPORTS=1> + ) +else () + target_compile_definitions(${PROJECT_NAME} PRIVATE + $<$:INFINIFRAME_BUILD_TEST_EXPORTS=1> + ) +endif () + # ---------------------------------------------------------------------------------------------------------------------- # Sanitizers (Debug only) # ---------------------------------------------------------------------------------------------------------------------- -if (CMAKE_BUILD_TYPE STREQUAL "Debug") - if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") - message(STATUS "Enabling sanitizers for Debug build") - target_compile_options(${PROJECT_NAME} PRIVATE - -fsanitize=address,undefined,leak - -fno-omit-frame-pointer - -fno-optimize-sibling-calls - ) - target_link_options(${PROJECT_NAME} PRIVATE - -fsanitize=address,undefined,leak - ) - endif () +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + message(STATUS "Enabling sanitizers for Debug configuration") + target_compile_options(${PROJECT_NAME} PRIVATE + $<$:-fsanitize=address,undefined,leak> + $<$:-fno-omit-frame-pointer> + $<$:-fno-optimize-sibling-calls> + ) + target_link_options(${PROJECT_NAME} PRIVATE + $<$:-fsanitize=address,undefined,leak> + ) endif () # ---------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrame.h b/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrame.h deleted file mode 100644 index cb38411cc..000000000 --- a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrame.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -/** - * @file InfiniFrame.h - * @brief Main header file for InfiniFrame native interop - * - * This file provides unified access to all InfiniFrame types and classes. - * It is the primary include file for C API consumers. - */ - -#ifndef INFINIFRAME_H -#define INFINIFRAME_H - -// --------------------------------------------------------------------------------------------------------------------- -// Core Types -// --------------------------------------------------------------------------------------------------------------------- - -#include "../Types/Basic.h" -#include "../Types/Dialog.h" -#include "../Types/Callbacks.h" -#include "InfiniFrameInitParams.h" - -// --------------------------------------------------------------------------------------------------------------------- -// Core Classes -// --------------------------------------------------------------------------------------------------------------------- - -#include "InfiniFrameWindow.h" -#include "InfiniFrameDialog.h" - -// --------------------------------------------------------------------------------------------------------------------- -// Utilities -// --------------------------------------------------------------------------------------------------------------------- - -#include "../Utils/Common.h" -#include "../Utils/Event.h" - -#endif // INFINIFRAME_H diff --git a/src/InfiniFrame.NativeBridge/Native/Dependencies/simdutf/simdutf.h b/src/InfiniFrame.NativeBridge/Native/Dependencies/simdutf/simdutf.h index b1af31254..65241451c 100644 --- a/src/InfiniFrame.NativeBridge/Native/Dependencies/simdutf/simdutf.h +++ b/src/InfiniFrame.NativeBridge/Native/Dependencies/simdutf/simdutf.h @@ -14157,4 +14157,4 @@ template consteval auto operator""_base64() { SIMDUTF_POP_DISABLE_WARNINGS #endif // SIMDUTF_H -/* end file include/simdutf.h */ +/* end file include/simdutf.h */ \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/Embedded/Embedded.h b/src/InfiniFrame.NativeBridge/Native/Embedded/Embedded.h index 6cfe33018..5c3d510eb 100644 --- a/src/InfiniFrame.NativeBridge/Native/Embedded/Embedded.h +++ b/src/InfiniFrame.NativeBridge/Native/Embedded/Embedded.h @@ -1,35 +1,36 @@ #pragma once - -#include "InfiniFrameJs.h" +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Embedded/InfiniFrameJs/InfiniFrameJs.h" #include - +#include +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- namespace Embedded { inline const std::wstring& InfiniFrameJsUtf16() { - static const std::wstring cached = [] { - const auto* src = reinterpret_cast(g_infiniframe_js_data); + static const std::wstring Cached = [] { + const auto* src = reinterpret_cast(GInfiniframeJsData); std::u16string temp; - temp.resize(simdutf::utf16_length_from_utf8(src, g_infiniframe_js_size)); + temp.resize(simdutf::utf16_length_from_utf8(src, GInfiniframeJsSize)); - const size_t written = simdutf::convert_utf8_to_utf16( - src, - g_infiniframe_js_size, - temp.data() - ); + const size_t written = simdutf::convert_utf8_to_utf16(src, GInfiniframeJsSize, temp.data()); temp.resize(written); return std::wstring(temp.begin(), temp.end()); }(); - return cached; + return Cached; } - + inline const std::string& InfiniFrameJsUtf8() { - static const std::string cached = [] { - const auto* src = reinterpret_cast(g_infiniframe_js_data); - return std::string(src, g_infiniframe_js_size); + static const std::string Cached = [] { + const auto* src = reinterpret_cast(GInfiniframeJsData); + return std::string(src, GInfiniframeJsSize); }(); - return cached; + return Cached; } } diff --git a/src/InfiniFrame.NativeBridge/Native/Embedded/InfiniFrameJs/InfiniFrameJs.h b/src/InfiniFrame.NativeBridge/Native/Embedded/InfiniFrameJs/InfiniFrameJs.h deleted file mode 100644 index f5a4595d5..000000000 --- a/src/InfiniFrame.NativeBridge/Native/Embedded/InfiniFrameJs/InfiniFrameJs.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once -// ReSharper disable once CppUnusedIncludeDirective -#include - -extern const unsigned char g_infiniframe_js_data[]; -extern const size_t g_infiniframe_js_size; diff --git a/src/InfiniFrame.NativeBridge/Native/Exports.cpp b/src/InfiniFrame.NativeBridge/Native/Exports.cpp deleted file mode 100644 index a463e6ccc..000000000 --- a/src/InfiniFrame.NativeBridge/Native/Exports.cpp +++ /dev/null @@ -1,774 +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); - } - - EXPORTED void InfiniFrame_setClosedClosedCallback(InfiniFrameWindow* instance, const ClosedCallback callback) - { instance->SetClosedCallback(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.NativeBridge/Native/Platform/Linux/Core/UiDispatcher.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/UiDispatcher.Gtk.cpp new file mode 100644 index 000000000..03680e670 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/UiDispatcher.Gtk.cpp @@ -0,0 +1,39 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +namespace { + std::mutex invokeLockMutex; + + struct InvokeWaitInfo { + ACTION callback; + std::condition_variable completionNotifier; + bool isCompleted; + }; + + 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; + } +} // namespace + +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; }); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowCore.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowCore.Gtk.cpp new file mode 100644 index 000000000..fd9fb7817 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowCore.Gtk.cpp @@ -0,0 +1,50 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) + : m_impl(std::make_unique()) { + XInitThreads(); + gtk_init(nullptr, nullptr); + notify_init(initParams->Title); + + if (initParams->StructSize != 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->StructSize, + sizeof(InfiniFrameInitParams) + ); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + exit(0); + } + + m_impl->InitializeFromParams(initParams); + m_impl->ConfigureInitialWindow(this, initParams); + m_impl->ApplyInitialWindowState(this, initParams); + m_impl->ConnectWindowSignals(this); + + // Register custom schemes before first navigation to avoid first-load races. + m_impl->AddCustomSchemeHandlers(); + + Show(false); + + m_impl->ConnectWebViewSignals(this); + + if (initParams->Transparent) + SetTransparentEnabled(true); + + if (m_impl->_zoom != 100.0) + SetZoom(m_impl->_zoom); +} + +InfiniFrameWindow::~InfiniFrameWindow() { + notify_uninit(); + gtk_widget_destroy(m_impl->_window); +} \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowEvents.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowEvents.Gtk.cpp new file mode 100644 index 000000000..dfcb4b017 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowEvents.Gtk.cpp @@ -0,0 +1,150 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +InfiniFrameDialog* InfiniFrameWindow::GetDialog() const { + return m_impl->_dialog.get(); +} + +void InfiniFrameWindow::AddCustomSchemeName(const AutoStringConst scheme) { + if (scheme == nullptr) { + return; + } + + m_impl->_customSchemeNames.emplace_back(scheme); +} + +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 MonitorCount = gdk_display_get_n_monitors(display); + + for (int i = 0; i < MonitorCount; 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) == 0) { + break; + } + } +} + +void InfiniFrameWindow::SetClosingCallback(const ClosingCallback callback) { + m_impl->_closingCallback = callback; +} + +void InfiniFrameWindow::SetClosedCallback(const ClosedCallback callback) { + m_impl->_closedCallback = callback; +} + +void InfiniFrameWindow::SetFocusInCallback(const FocusInCallback callback) { + m_impl->_focusInCallback = callback; +} + +void InfiniFrameWindow::SetFocusOutCallback(const FocusOutCallback callback) { + m_impl->_focusOutCallback = callback; +} + +void InfiniFrameWindow::SetMovedCallback(const MovedCallback callback) { + m_impl->_movedCallback = callback; +} + +void InfiniFrameWindow::SetResizedCallback(const ResizedCallback callback) { + m_impl->_resizedCallback = callback; +} + +void InfiniFrameWindow::SetMaximizedCallback(const MaximizedCallback callback) { + m_impl->_maximizedCallback = callback; +} + +void InfiniFrameWindow::SetRestoredCallback(const RestoredCallback callback) { + m_impl->_restoredCallback = callback; +} + +void InfiniFrameWindow::SetMinimizedCallback(const MinimizedCallback callback) { + m_impl->_minimizedCallback = callback; +} + +[[nodiscard]] bool InfiniFrameWindow::InvokeClose() const noexcept { + if (m_impl->_closingCallback == nullptr) { + return false; + } + + return m_impl->_closingCallback(); +} + +void InfiniFrameWindow::InvokeClosed() const noexcept { + if (m_impl->_closedCallback == nullptr) { + return; + } + + m_impl->_closedCallback(); +} + +void InfiniFrameWindow::InvokeFocusIn() const noexcept { + if (m_impl->_focusInCallback == nullptr) { + return; + } + + m_impl->_focusInCallback(); +} + +void InfiniFrameWindow::InvokeFocusOut() const noexcept { + if (m_impl->_focusOutCallback == nullptr) { + return; + } + + m_impl->_focusOutCallback(); +} + +void InfiniFrameWindow::InvokeMove(int x, int y) const noexcept { + if (m_impl->_movedCallback == nullptr) { + return; + } + + m_impl->_movedCallback(x, y); +} + +void InfiniFrameWindow::InvokeResize(int width, int height) const noexcept { + if (m_impl->_resizedCallback == nullptr) { + return; + } + + m_impl->_resizedCallback(width, height); +} + +void InfiniFrameWindow::InvokeMaximized() const noexcept { + if (m_impl->_maximizedCallback == nullptr) { + return; + } + + m_impl->_maximizedCallback(); +} + +void InfiniFrameWindow::InvokeRestored() const noexcept { + if (m_impl->_restoredCallback == nullptr) { + return; + } + + m_impl->_restoredCallback(); +} + +void InfiniFrameWindow::InvokeMinimized() const noexcept { + if (m_impl->_minimizedCallback == nullptr) { + return; + } + + m_impl->_minimizedCallback(); +} \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowInitialization.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowInitialization.Gtk.cpp new file mode 100644 index 000000000..246a1018b --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowInitialization.Gtk.cpp @@ -0,0 +1,173 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Public/InfiniFrameDialog.h" +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +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); +void on_widget_destroyed(GtkWidget* widget, gpointer self); +gboolean on_focus_in_event(GtkWidget* widget, GdkEvent* event, gpointer self); +gboolean on_focus_out_event(GtkWidget* widget, GdkEvent* event, gpointer self); +gboolean on_webview_context_menu( + WebKitWebView* web_view, + GtkWidget* default_menu, + WebKitHitTestResult* hit_test_result, + gboolean triggered_with_keyboard, + gpointer user_data +); +gboolean on_permission_request(WebKitWebView* web_view, WebKitPermissionRequest* request, gpointer user_data); + +void InfiniFrameWindow::Impl::InitializeFromParams(const InfiniFrameInitParams* initParams) { + if (initParams->Title != nullptr) { + _windowTitle = initParams->Title; + } else { + _windowTitle = ""; + } + + if (initParams->StartUrl != nullptr) { + _startUrl = initParams->StartUrl; + } + if (initParams->StartString != nullptr) { + _startString = initParams->StartString; + } + if (initParams->TemporaryFilesPath != nullptr) { + _temporaryFilesPath = initParams->TemporaryFilesPath; + } + if (initParams->UserAgent != nullptr) { + _userAgent = initParams->UserAgent; + } + if (initParams->BrowserControlInitParameters != nullptr) { + _browserControlInitParameters = initParams->BrowserControlInitParameters; + } + + _transparentEnabled = initParams->Transparent; + _contextMenuEnabled = initParams->ContextMenuEnabled; + _zoomEnabled = initParams->ZoomEnabled; + _devToolsEnabled = initParams->DevToolsEnabled; + _grantBrowserPermissions = initParams->GrantBrowserPermissions; + _mediaAutoplayEnabled = initParams->MediaAutoplayEnabled; + _fileSystemAccessEnabled = initParams->FileSystemAccessEnabled; + _webSecurityEnabled = initParams->WebSecurityEnabled; + _javascriptClipboardAccessEnabled = initParams->JavascriptClipboardAccessEnabled; + _mediaStreamEnabled = initParams->MediaStreamEnabled; + _smoothScrollingEnabled = initParams->SmoothScrollingEnabled; + _ignoreCertificateErrorsEnabled = initParams->IgnoreCertificateErrorsEnabled; + _isFullScreen = initParams->FullScreen; + + _zoom = initParams->Zoom; + _minWidth = initParams->MinWidth; + _minHeight = initParams->MinHeight; + _maxWidth = initParams->MaxWidth; + _maxHeight = initParams->MaxHeight; + + _webMessageReceivedCallback = initParams->WebMessageReceivedHandler; + _resizedCallback = initParams->ResizedHandler; + _movedCallback = initParams->MovedHandler; + _closingCallback = initParams->ClosingHandler; + _closedCallback = initParams->ClosedHandler; + _focusInCallback = initParams->FocusInHandler; + _focusOutCallback = initParams->FocusOutHandler; + _maximizedCallback = initParams->MaximizedHandler; + _minimizedCallback = initParams->MinimizedHandler; + _restoredCallback = initParams->RestoredHandler; + _customSchemeCallback = initParams->CustomSchemeHandler; + + _customSchemeNames.clear(); + for (auto* customSchemeName : initParams->CustomSchemeNames) { + if (customSchemeName == nullptr) { + continue; + } + _customSchemeNames.emplace_back(customSchemeName); + } + + _parent = initParams->ParentInstance; +} + +void InfiniFrameWindow::Impl::ConfigureInitialWindow(InfiniFrameWindow* window, InfiniFrameInitParams* initParams) { + _window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + _dialog = std::make_unique(); + + if (initParams->FullScreen) { + window->SetFullScreen(true); + return; + } + + initParams->Width = std::min(initParams->Width, initParams->MaxWidth); + initParams->Height = std::min(initParams->Height, initParams->MaxHeight); + initParams->Width = std::max(initParams->Width, initParams->MinWidth); + initParams->Height = std::max(initParams->Height, initParams->MinHeight); + + if (initParams->UseOsDefaultSize) { + gtk_window_set_default_size(GTK_WINDOW(_window), -1, -1); + } else { + gtk_window_set_default_size(GTK_WINDOW(_window), initParams->Width, initParams->Height); + } + + window->SetMinSize(initParams->MinWidth, initParams->MinHeight); + window->SetMaxSize(initParams->MaxWidth, initParams->MaxHeight); + + if (initParams->UseOsDefaultLocation) { + gtk_window_set_position(GTK_WINDOW(_window), GTK_WIN_POS_NONE); + } else if (initParams->CenterOnInitialize) { + gtk_window_set_position(GTK_WINDOW(_window), GTK_WIN_POS_CENTER); + } else { + gtk_window_move(GTK_WINDOW(_window), initParams->Left, initParams->Top); + } +} + +void InfiniFrameWindow::Impl::ApplyInitialWindowState( + InfiniFrameWindow* window, const InfiniFrameInitParams* initParams +) { + window->SetTitle(const_cast(_windowTitle.c_str())); + + if (initParams->Chromeless) { + gtk_window_set_decorated(GTK_WINDOW(_window), false); + } + + if (initParams->WindowIconFile != nullptr && std::strlen(initParams->WindowIconFile) > 0) { + window->SetIconFile(initParams->WindowIconFile); + } + + if (initParams->CenterOnInitialize) { + window->Center(); + } + if (initParams->Minimized) { + window->SetMinimized(true); + } + if (initParams->Maximized) { + window->SetMaximized(true); + } + if (!initParams->Resizable) { + window->SetResizable(false); + } + if (initParams->Topmost) { + window->SetTopmost(true); + } +} + +void InfiniFrameWindow::Impl::ConnectWindowSignals(InfiniFrameWindow* window) { + g_signal_connect(G_OBJECT(_window), "configure-event", G_CALLBACK(on_configure_event), window); + + g_signal_connect(G_OBJECT(_window), "window-state-event", G_CALLBACK(on_window_state_event), window); + + g_signal_connect(G_OBJECT(_window), "delete-event", G_CALLBACK(on_widget_deleted), window); + + g_signal_connect(G_OBJECT(_window), "destroy", G_CALLBACK(on_widget_destroyed), window); + + g_signal_connect(G_OBJECT(_window), "focus-in-event", G_CALLBACK(on_focus_in_event), window); + + g_signal_connect(G_OBJECT(_window), "focus-out-event", G_CALLBACK(on_focus_out_event), window); +} + +void InfiniFrameWindow::Impl::ConnectWebViewSignals(InfiniFrameWindow* window) { + g_signal_connect(G_OBJECT(_webview), "context-menu", G_CALLBACK(on_webview_context_menu), window); + + g_signal_connect(G_OBJECT(_webview), "permission-request", G_CALLBACK(on_permission_request), window); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowLifecycle.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowLifecycle.Gtk.cpp new file mode 100644 index 000000000..bb7c479b0 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowLifecycle.Gtk.cpp @@ -0,0 +1,70 @@ + // --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include + +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +void InfiniFrameWindow::Center() { + gint windowWidth, windowHeight; + gtk_window_get_size(GTK_WINDOW(m_impl->_window), &windowWidth, &windowHeight); + + GdkRectangle screen = {}; + + GdkDisplay* display = gdk_display_get_default(); + if (display == nullptr) { + GtkWidget* dialog = gtk_message_dialog_new( + nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + "gdk_display_get_default() returned NULL" + ); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return; + } + + GdkMonitor* monitor = gdk_display_get_primary_monitor(display); + if (monitor == nullptr) { + monitor = gdk_display_get_monitor(display, 0); + if (monitor == nullptr) { + GtkWidget* dialog = gtk_message_dialog_new( + nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + "gdk_display_get_primary_monitor() returned NULL" + ); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return; + } + } + + gdk_monitor_get_geometry(monitor, &screen); + + gtk_window_move(GTK_WINDOW(m_impl->_window), (screen.width - windowWidth) / 2, (screen.height - windowHeight) / 2); +} + +void InfiniFrameWindow::ClearBrowserAutoFill() { + // TODO +} + +void InfiniFrameWindow::Close() { + gtk_window_close(GTK_WINDOW(m_impl->_window)); +} + +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( + G_OBJECT(m_impl->_window), "destroy", G_CALLBACK(+[](GtkWidget*, gpointer) { gtk_main_quit(); }), nullptr + ); + gtk_main(); +} + +void InfiniFrameWindow::CloseWebView() { + // Not implemented on Linux +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowSignals.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowSignals.Gtk.cpp new file mode 100644 index 000000000..9e83ed3e0 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowSignals.Gtk.cpp @@ -0,0 +1,171 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include + +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +namespace { + bool linux_webview_diagnostics_enabled() { + const char* value = g_getenv("INFINIFRAME_LINUX_WEBVIEW_DIAGNOSTICS"); + return value != nullptr && value[0] != '\0' && g_strcmp0(value, "0") != 0; + } + + const char* webkit_load_event_to_string(WebKitLoadEvent event) { + switch (event) { + case WEBKIT_LOAD_STARTED: + return "started"; + case WEBKIT_LOAD_REDIRECTED: + return "redirected"; + case WEBKIT_LOAD_COMMITTED: + return "committed"; + case WEBKIT_LOAD_FINISHED: + return "finished"; + default: + return "unknown"; + } + } + + const char* webkit_termination_reason_to_string(WebKitWebProcessTerminationReason reason) { + switch (reason) { + case WEBKIT_WEB_PROCESS_CRASHED: + return "crashed"; + case WEBKIT_WEB_PROCESS_EXCEEDED_MEMORY_LIMIT: + return "exceeded-memory-limit"; + case WEBKIT_WEB_PROCESS_TERMINATED_BY_API: + return "terminated-by-api"; + default: + return "unknown"; + } + } +} // namespace + +void InfiniFrameWindow::OnConfigureEvent(int x, int y, int width, int height) { + if (m_impl->_lastLeft != x || m_impl->_lastTop != y) { + InvokeMove(x, y); + m_impl->_lastLeft = x; + m_impl->_lastTop = y; + } + + if (m_impl->_lastHeight != height || m_impl->_lastWidth != width) { + InvokeResize(width, height); + m_impl->_lastWidth = width; + m_impl->_lastHeight = height; + } +} + +void InfiniFrameWindow::OnWindowStateEvent(GdkWindowState newState) { + if (newState & GDK_WINDOW_STATE_MAXIMIZED) { + InvokeMaximized(); + } else if ((newState & GDK_WINDOW_STATE_ICONIFIED) || !gtk_widget_get_mapped(m_impl->_window)) { + InvokeMinimized(); + } else if (!(newState & GDK_WINDOW_STATE_MAXIMIZED) && !(newState & GDK_WINDOW_STATE_ICONIFIED)) { + InvokeRestored(); + } +} + +gboolean on_configure_event(GtkWidget* widget, GdkEvent* event, const gpointer self) { + if (event->type == GDK_CONFIGURE) { + auto* instance = reinterpret_cast(self); + instance->OnConfigureEvent( + event->configure.x, event->configure.y, event->configure.width, event->configure.height + ); + } + return FALSE; +} + +gboolean on_window_state_event(GtkWidget* widget, GdkEventWindowState* event, const gpointer self) { + auto* instance = reinterpret_cast(self); + instance->OnWindowStateEvent(event->new_window_state); + return TRUE; +} + +gboolean on_widget_deleted(GtkWidget* widget, GdkEvent* event, const gpointer self) { + auto* instance = reinterpret_cast(self); + return instance->InvokeClose(); +} + +void on_widget_destroyed(GtkWidget* widget, const gpointer self) { + auto* instance = reinterpret_cast(self); + instance->InvokeClosed(); +} + +gboolean on_focus_in_event(GtkWidget* widget, GdkEvent* event, const gpointer self) { + auto* instance = reinterpret_cast(self); + instance->InvokeFocusIn(); + return FALSE; +} + +gboolean on_focus_out_event(GtkWidget* widget, GdkEvent* event, const gpointer self) { + auto* instance = reinterpret_cast(self); + instance->InvokeFocusOut(); + return FALSE; +} + +gboolean on_webview_context_menu( + WebKitWebView* web_view, + GtkWidget* default_menu, + WebKitHitTestResult* hit_test_result, + gboolean triggered_with_keyboard, + const gpointer self +) { + auto* instance = reinterpret_cast(self); + bool contextMenuEnabled = false; + instance->GetContextMenuEnabled(&contextMenuEnabled); + return !contextMenuEnabled; +} + +gboolean on_permission_request(WebKitWebView* web_view, WebKitPermissionRequest* request, gpointer user_data) { + auto* instance = reinterpret_cast(user_data); + bool grant = false; + instance->GetGrantBrowserPermissions(&grant); + if (grant) + webkit_permission_request_allow(request); + else + webkit_permission_request_deny(request); + return TRUE; +} + +void on_webview_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event, gpointer user_data) { + if (!linux_webview_diagnostics_enabled()) + return; + + const char* uri = webkit_web_view_get_uri(web_view); + g_message( + "[InfiniFrame/Linux] WebKit load-changed: event=%s uri=%s", webkit_load_event_to_string(load_event), + uri ? uri : "" + ); +} + +gboolean on_webview_load_failed( + WebKitWebView* web_view, WebKitLoadEvent load_event, gchar* failing_uri, GError* error, gpointer user_data +) { + if (!linux_webview_diagnostics_enabled()) + return FALSE; + + g_warning( + "[InfiniFrame/Linux] WebKit load-failed: event=%s uri=%s error=%s", webkit_load_event_to_string(load_event), + failing_uri ? failing_uri : "", error ? error->message : "" + ); + return FALSE; +} + +void on_webview_process_terminated( + WebKitWebView* web_view, WebKitWebProcessTerminationReason reason, gpointer user_data +) { + g_warning( + "[InfiniFrame/Linux] WebKit web process terminated: reason=%s", webkit_termination_reason_to_string(reason) + ); +} + +void on_webview_size_allocate(GtkWidget* widget, GtkAllocation* allocation, gpointer user_data) { + if (!linux_webview_diagnostics_enabled()) + return; + + g_message( + "[InfiniFrame/Linux] WebView size-allocate: %dx%d", allocation ? allocation->width : -1, + allocation ? allocation->height : -1 + ); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowState.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowState.Gtk.cpp new file mode 100644 index 000000000..e0337e3d7 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Core/WindowState.Gtk.cpp @@ -0,0 +1,325 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Utils/Common.h" +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +void InfiniFrameWindow::GetTransparentEnabled(bool* enabled) const { + *enabled = m_impl->_transparentEnabled; +} + +void InfiniFrameWindow::GetContextMenuEnabled(bool* enabled) const { + *enabled = m_impl->_contextMenuEnabled; +} + +void InfiniFrameWindow::GetZoomEnabled(bool* enabled) const { + *enabled = m_impl->_zoomEnabled; +} + +void InfiniFrameWindow::GetDevToolsEnabled(bool* enabled) const { + WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_impl->_webview)); + *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; +} + +AutoString InfiniFrameWindow::GetUserAgent() const { + return AllocateStringCopy(m_impl->_userAgent); +} + +void InfiniFrameWindow::GetMediaAutoplayEnabled(bool* enabled) const { + *enabled = m_impl->_mediaAutoplayEnabled; +} + +void InfiniFrameWindow::GetFileSystemAccessEnabled(bool* enabled) const { + *enabled = m_impl->_fileSystemAccessEnabled; +} + +void InfiniFrameWindow::GetWebSecurityEnabled(bool* enabled) const { + *enabled = m_impl->_webSecurityEnabled; +} + +void InfiniFrameWindow::GetJavascriptClipboardAccessEnabled(bool* enabled) const { + *enabled = m_impl->_javascriptClipboardAccessEnabled; +} + +void InfiniFrameWindow::GetMediaStreamEnabled(bool* enabled) const { + *enabled = m_impl->_mediaStreamEnabled; +} + +void InfiniFrameWindow::GetSmoothScrollingEnabled(bool* enabled) const { + *enabled = m_impl->_smoothScrollingEnabled; +} + +void InfiniFrameWindow::GetIgnoreCertificateErrorsEnabled(bool* enabled) const { + *enabled = m_impl->_ignoreCertificateErrorsEnabled; +} + +void InfiniFrameWindow::GetMaximized(bool* isMaximized) const { + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(m_impl->_window)); + GdkWindowState flags = gdk_window_get_state(gdk_window); + *isMaximized = flags & GDK_WINDOW_STATE_MAXIMIZED; +} + +void InfiniFrameWindow::GetMinimized(bool* isMinimized) const { + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(m_impl->_window)); + GdkWindowState flags = gdk_window_get_state(gdk_window); + *isMinimized = flags & GDK_WINDOW_STATE_ICONIFIED; +} + +void InfiniFrameWindow::GetPosition(int* x, int* y) const { + gtk_window_get_position(GTK_WINDOW(m_impl->_window), x, y); +} + +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); +} + +void InfiniFrameWindow::GetMaxSize(int* width, int* height) const { + if (width) + *width = m_impl->_maxWidth; + if (height) + *height = m_impl->_maxHeight; +} + +void InfiniFrameWindow::GetMinSize(int* width, int* height) const { + if (width) + *width = m_impl->_minWidth; + if (height) + *height = m_impl->_minHeight; +} + +AutoString InfiniFrameWindow::GetTitle() const { + const char* title = gtk_window_get_title(GTK_WINDOW(m_impl->_window)); + return g_strdup(title ? title : ""); +} + +void InfiniFrameWindow::GetTopmost(bool* topmost) const { + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(m_impl->_window)); + GdkWindowState flags = gdk_window_get_state(gdk_window); + *topmost = flags & GDK_WINDOW_STATE_ABOVE; +} + +void InfiniFrameWindow::GetZoom(int* zoom) const { + double rawValue = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(m_impl->_webview)); + rawValue = (rawValue * 100.0) + 0.5; + *zoom = static_cast(rawValue); +} + +void InfiniFrameWindow::GetFocused(bool* isFocused) const { + *isFocused = gtk_window_is_active(GTK_WINDOW(m_impl->_window)); +} + +AutoString InfiniFrameWindow::GetIconFileName() const { + return AllocateStringCopy(m_impl->_iconFileName); +} + +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 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; +} + +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 + ); +} + +void InfiniFrameWindow::SetContextMenuEnabled(const bool enabled) { + m_impl->_contextMenuEnabled = enabled; +} + +void InfiniFrameWindow::SetZoomEnabled(bool enabled) { + (void)enabled; +} + +void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { + m_impl->_devToolsEnabled = enabled; + WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_impl->_webview)); + 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); +} + +void InfiniFrameWindow::SetResizable(const bool resizable) { + gtk_window_set_resizable(GTK_WINDOW(m_impl->_window), resizable); +} + +void InfiniFrameWindow::SetMinSize(const int width, const int height) { + m_impl->_minWidth = width; + m_impl->_minHeight = height; + m_impl->_hints.min_width = width; + m_impl->_hints.min_height = height; + + gtk_window_set_geometry_hints( + GTK_WINDOW(m_impl->_window), nullptr, &m_impl->_hints, + static_cast(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE) + ); +} + +void InfiniFrameWindow::SetMaxSize(const int width, const int height) { + m_impl->_maxWidth = width; + m_impl->_maxHeight = height; + m_impl->_hints.max_width = width; + m_impl->_hints.max_height = height; + + gtk_window_set_geometry_hints( + GTK_WINDOW(m_impl->_window), nullptr, &m_impl->_hints, + static_cast(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE) + ); +} + +void InfiniFrameWindow::SetSize(const int width, const int height) { + gtk_window_resize(GTK_WINDOW(m_impl->_window), width, height); +} + +void InfiniFrameWindow::SetTitle(const AutoString title) { + gtk_window_set_title(GTK_WINDOW(m_impl->_window), title); +} + +void InfiniFrameWindow::SetTopmost(const bool topmost) { + gtk_window_set_keep_above(GTK_WINDOW(m_impl->_window), topmost); +} + +void InfiniFrameWindow::SetZoom(const int zoom) { + double newZoom = zoom / 100.0; + webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(m_impl->_webview), newZoom); +} + +void InfiniFrameWindow::SetFocused() { + gtk_window_present(GTK_WINDOW(m_impl->_window)); +} + +void InfiniFrameWindow::SetTransparentEnabled(const bool enabled) { + m_impl->_transparentEnabled = enabled; + + gtk_window_set_decorated(GTK_WINDOW(m_impl->_window), !enabled); + + GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_impl->_window)); + GdkVisual* rgba_visual = gdk_screen_get_rgba_visual(screen); + if (rgba_visual) { + gtk_widget_set_visual(GTK_WIDGET(m_impl->_window), rgba_visual); + gtk_widget_set_app_paintable(GTK_WIDGET(m_impl->_window), true); + + GdkRGBA color; + webkit_web_view_get_background_color(WEBKIT_WEB_VIEW(m_impl->_webview), &color); + color.alpha = enabled ? 0 : 1; + webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(m_impl->_webview), &color); + } +} \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Dialog.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Dialog.cpp index 43b1c0d9c..1f7ccc5cd 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Dialog.cpp +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Dialog.cpp @@ -1,12 +1,12 @@ -#ifdef __linux__ -/** - * @file Dialog.cpp (Linux) - * @brief Linux implementation of InfiniFrameDialog using GTK3 file-chooser and message dialogs - */ - -#include "Core/InfiniFrameDialog.h" +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #include +#include "Public/InfiniFrameDialog.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- /** @brief Distinguishes which GtkFileChooserAction to configure in ShowDialog */ enum DialogType { OpenFile, /// GTK_FILE_CHOOSER_ACTION_OPEN — select one or more files @@ -69,7 +69,7 @@ AutoString* ShowDialog( const int filterCount, int* resultCount, const AutoString defaultFileName = nullptr - ) { +) { GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; const char* buttonText = "_Open"; switch (type) { @@ -88,11 +88,8 @@ AutoString* ShowDialog( } GtkWidget* dialog = gtk_file_chooser_dialog_new( - title, nullptr, action, - "_Cancel", GTK_RESPONSE_CANCEL, - buttonText, GTK_RESPONSE_ACCEPT, - nullptr - ); + title, nullptr, action, "_Cancel", GTK_RESPONSE_CANCEL, buttonText, GTK_RESPONSE_ACCEPT, nullptr + ); if (defaultPath != nullptr) { gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), defaultPath); @@ -130,19 +127,16 @@ AutoString* ShowDialog( *resultCount = count; gtk_widget_destroy(dialog); return results; - } - else { + } else { char* result = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); gtk_widget_destroy(dialog); return new char*[1]{result}; } } -InfiniFrameDialog::InfiniFrameDialog() { -} +InfiniFrameDialog::InfiniFrameDialog() {} -InfiniFrameDialog::~InfiniFrameDialog() { -} +InfiniFrameDialog::~InfiniFrameDialog() {} AutoString* InfiniFrameDialog::ShowOpenFile( const AutoString title, @@ -151,16 +145,13 @@ AutoString* InfiniFrameDialog::ShowOpenFile( AutoString* filters, const int filterCount, int* resultCount - ) { +) { return ShowDialog(OpenFile, title, defaultPath, multiSelect, filters, filterCount, resultCount); } AutoString* InfiniFrameDialog::ShowOpenFolder( - const AutoString title, - const AutoString defaultPath, - const bool multiSelect, - int* resultCount - ) { + const AutoString title, const AutoString defaultPath, const bool multiSelect, int* resultCount +) { return ShowDialog(OpenFolder, title, defaultPath, multiSelect, nullptr, 0, resultCount); } @@ -170,7 +161,7 @@ AutoString InfiniFrameDialog::ShowSaveFile( AutoString* filters, const int filterCount, const AutoString defaultFileName - ) { +) { char** result = ShowDialog(SaveFile, title, defaultPath, false, filters, filterCount, nullptr, defaultFileName); if (result != nullptr) { char* value = result[0]; @@ -181,11 +172,8 @@ AutoString InfiniFrameDialog::ShowSaveFile( } DialogResult InfiniFrameDialog::ShowMessage( - const AutoString title, - const AutoString text, - const DialogButtons buttons, - const DialogIcon icon - ) { + const AutoString title, const AutoString text, const DialogButtons buttons, const DialogIcon icon +) { GtkWidget* dialog; GtkMessageType type; @@ -207,14 +195,7 @@ DialogResult InfiniFrameDialog::ShowMessage( break; } - dialog = gtk_message_dialog_new( - nullptr, - GTK_DIALOG_MODAL, - type, - GTK_BUTTONS_NONE, - "%s", - title - ); + dialog = gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, type, GTK_BUTTONS_NONE, "%s", title); gtk_message_dialog_set_markup(GTK_MESSAGE_DIALOG(dialog), text); switch (buttons) { @@ -272,4 +253,3 @@ DialogResult InfiniFrameDialog::ShowMessage( return DialogResult::Cancel; } } -#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKit.Gtk.Internal.h b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKit.Gtk.Internal.h new file mode 100644 index 000000000..1791398ba --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKit.Gtk.Internal.h @@ -0,0 +1,15 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +namespace gtk_webkit { + void HandleWebMessage( + WebKitUserContentManager* contentManager, WebKitJavascriptResult* jsResult, gpointer userData + ); + + void HandleCustomSchemeRequest(WebKitURISchemeRequest* request, gpointer userData); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitCustomSchemes.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitCustomSchemes.Gtk.cpp new file mode 100644 index 000000000..08fb18952 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitCustomSchemes.Gtk.cpp @@ -0,0 +1,52 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Platform/Linux/Window.Gtk.Internal.h" +#include "Platform/Linux/WebKit/WebKit.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +namespace gtk_webkit { + 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); + } +} // namespace gtk_webkit + +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) { + webkit_security_manager_register_uri_scheme_as_secure(securityManager, value.c_str()); + } + + webkit_web_context_register_uri_scheme( + context, value.c_str(), + reinterpret_cast(gtk_webkit::HandleCustomSchemeRequest), + reinterpret_cast(_customSchemeCallback), nullptr + ); + } +} \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitHost.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitHost.Gtk.cpp new file mode 100644 index 000000000..a4829d224 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitHost.Gtk.cpp @@ -0,0 +1,82 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Embedded/Embedded.h" +#include "Platform/Linux/WebKit/WebKit.Gtk.Internal.h" +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern void on_webview_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event, gpointer user_data); +extern gboolean on_webview_load_failed( + WebKitWebView* web_view, WebKitLoadEvent load_event, gchar* failing_uri, GError* error, gpointer user_data +); +extern void on_webview_process_terminated( + WebKitWebView* web_view, WebKitWebProcessTerminationReason reason, gpointer user_data +); +extern void on_webview_size_allocate(GtkWidget* widget, GtkAllocation* allocation, gpointer user_data); + +void InfiniFrameWindow::Show(bool isAlreadyShown) { + if (m_impl->_webview) { + return; + } + + struct sigaction oldAction{}; + sigaction(SIGCHLD, nullptr, &oldAction); + WebKitUserContentManager* contentManager = webkit_user_content_manager_new(); + m_impl->_webview = webkit_web_view_new_with_user_content_manager(contentManager); + + m_impl->set_webkit_settings(); + + gtk_container_add(GTK_CONTAINER(m_impl->_window), m_impl->_webview); + gtk_widget_set_hexpand(m_impl->_webview, TRUE); + gtk_widget_set_vexpand(m_impl->_webview, TRUE); + + const auto& jsCode = Embedded::InfiniFrameJsUtf8(); + + WebKitUserScript* script = webkit_user_script_new( + jsCode.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(gtk_webkit::HandleWebMessage), + reinterpret_cast(m_impl->_webMessageReceivedCallback) + ); + webkit_user_content_manager_register_script_message_handler(contentManager, "infiniFrameInterop"); + + g_signal_connect(G_OBJECT(m_impl->_webview), "load-changed", G_CALLBACK(on_webview_load_changed), this); + g_signal_connect(G_OBJECT(m_impl->_webview), "load-failed", G_CALLBACK(on_webview_load_failed), this); + g_signal_connect( + G_OBJECT(m_impl->_webview), "web-process-terminated", G_CALLBACK(on_webview_process_terminated), this + ); + g_signal_connect(G_OBJECT(m_impl->_webview), "size-allocate", G_CALLBACK(on_webview_size_allocate), this); + + 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, &oldAction, nullptr); + return; + } + sigaction(SIGCHLD, &oldAction, nullptr); + + gtk_widget_show_all(m_impl->_window); +} + +void InfiniFrameWindow::AttachWebView() { + // On Linux, WebView is attached in Show() +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitMessaging.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitMessaging.Gtk.cpp new file mode 100644 index 000000000..981322b9a --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitMessaging.Gtk.cpp @@ -0,0 +1,49 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Types/Basic.h" +#include "Types/Callbacks.h" +#include "Platform/Linux/WebKit/WebKit.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +namespace gtk_webkit { + 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); + auto 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); + } +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitSettings.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitSettings.Gtk.cpp new file mode 100644 index 000000000..40c0da1e8 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/WebKit/WebKitSettings.Gtk.cpp @@ -0,0 +1,99 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include + +#include "Platform/Linux/Window.Gtk.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +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: + break; + } + + if (hasValidValue) { + g_object_set_property(G_OBJECT(settings), propertyName, &propertyValue); + g_value_unset(&propertyValue); + } + + g_free(propertyName); + } + } catch (const simdjson::simdjson_error&) {} +} \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Window.Gtk.Internal.h b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Window.Gtk.Internal.h new file mode 100644 index 000000000..0be61c9d7 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Window.Gtk.Internal.h @@ -0,0 +1,43 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include +#include +#include + +#include "Public/InfiniFrameWindow.h" +#include "Public/InfiniFrameWindowImpl.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +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(); + void InitializeFromParams(const InfiniFrameInitParams* initParams); + void ConfigureInitialWindow(InfiniFrameWindow* window, InfiniFrameInitParams* initParams); + void ApplyInitialWindowState(InfiniFrameWindow* window, const InfiniFrameInitParams* initParams); + void ConnectWindowSignals(InfiniFrameWindow* window); + void ConnectWebViewSignals(InfiniFrameWindow* window); +}; diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Window.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Window.cpp deleted file mode 100644 index 0dc0a7de8..000000000 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Linux/Window.cpp +++ /dev/null @@ -1,1284 +0,0 @@ -#ifdef __linux__ -#include "Core/InfiniFrameWindow.h" -#include "Core/InfiniFrameDialog.h" -#include "Core/InfiniFrameWindowImpl.h" -#include "Utils/Common.h" -#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 -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); -void on_widget_destroyed(GtkWidget* widget, gpointer self); -gboolean on_focus_in_event(GtkWidget* widget, GdkEvent* event, gpointer self); -gboolean on_focus_out_event(GtkWidget* widget, GdkEvent* event, gpointer self); -gboolean on_webview_context_menu( - WebKitWebView* web_view, - GtkWidget* default_menu, - WebKitHitTestResult* hit_test_result, - gboolean triggered_with_keyboard, - gpointer user_data - ); -gboolean on_permission_request(WebKitWebView* web_view, WebKitPermissionRequest* request, gpointer user_data); -void on_webview_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event, gpointer user_data); -gboolean on_webview_load_failed( - WebKitWebView* web_view, - WebKitLoadEvent load_event, - gchar* failing_uri, - GError* error, - gpointer user_data - ); -void on_webview_process_terminated( - WebKitWebView* web_view, - WebKitWebProcessTerminationReason reason, - gpointer user_data - ); -void on_webview_size_allocate(GtkWidget* widget, GtkAllocation* allocation, 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; -} - -static bool linux_webview_diagnostics_enabled() { - const char* value = g_getenv("INFINIFRAME_LINUX_WEBVIEW_DIAGNOSTICS"); - return value != nullptr && value[0] != '\0' && g_strcmp0(value, "0") != 0; -} - -static const char* webkit_load_event_to_string(WebKitLoadEvent event) { - switch (event) { - case WEBKIT_LOAD_STARTED: - return "started"; - case WEBKIT_LOAD_REDIRECTED: - return "redirected"; - case WEBKIT_LOAD_COMMITTED: - return "committed"; - case WEBKIT_LOAD_FINISHED: - return "finished"; - default: - return "unknown"; - } -} - -static const char* webkit_termination_reason_to_string(WebKitWebProcessTerminationReason reason) { - switch (reason) { - case WEBKIT_WEB_PROCESS_CRASHED: - return "crashed"; - case WEBKIT_WEB_PROCESS_EXCEEDED_MEMORY_LIMIT: - return "exceeded-memory-limit"; - case WEBKIT_WEB_PROCESS_TERMINATED_BY_API: - return "terminated-by-api"; - default: - return "unknown"; - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// 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()) { - 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->_windowTitle = initParams->Title ? initParams->Title : ""; - - if (initParams->StartUrl != nullptr) - m_impl->_startUrl = initParams->StartUrl; - - if (initParams->StartString != nullptr) - m_impl->_startString = initParams->StartString; - - if (initParams->TemporaryFilesPath != nullptr) - m_impl->_temporaryFilesPath = initParams->TemporaryFilesPath; - - if (initParams->UserAgent != nullptr) - m_impl->_userAgent = initParams->UserAgent; - - if (initParams->BrowserControlInitParameters != nullptr) - m_impl->_browserControlInitParameters = initParams->BrowserControlInitParameters; - - m_impl->_transparentEnabled = initParams->Transparent; - m_impl->_contextMenuEnabled = initParams->ContextMenuEnabled; - m_impl->_zoomEnabled = initParams->ZoomEnabled; - m_impl->_devToolsEnabled = initParams->DevToolsEnabled; - m_impl->_grantBrowserPermissions = initParams->GrantBrowserPermissions; - m_impl->_mediaAutoplayEnabled = initParams->MediaAutoplayEnabled; - m_impl->_fileSystemAccessEnabled = initParams->FileSystemAccessEnabled; - m_impl->_webSecurityEnabled = initParams->WebSecurityEnabled; - m_impl->_javascriptClipboardAccessEnabled = initParams->JavascriptClipboardAccessEnabled; - m_impl->_mediaStreamEnabled = initParams->MediaStreamEnabled; - m_impl->_smoothScrollingEnabled = initParams->SmoothScrollingEnabled; - m_impl->_ignoreCertificateErrorsEnabled = initParams->IgnoreCertificateErrorsEnabled; - m_impl->_isFullScreen = initParams->FullScreen; - - m_impl->_zoom = initParams->Zoom; - m_impl->_minWidth = initParams->MinWidth; - m_impl->_minHeight = initParams->MinHeight; - m_impl->_maxWidth = initParams->MaxWidth; - m_impl->_maxHeight = initParams->MaxHeight; - - m_impl->_webMessageReceivedCallback = initParams->WebMessageReceivedHandler; - m_impl->_resizedCallback = initParams->ResizedHandler; - m_impl->_movedCallback = initParams->MovedHandler; - m_impl->_closingCallback = initParams->ClosingHandler; - m_impl->_closedCallback = initParams->ClosedHandler; - m_impl->_focusInCallback = initParams->FocusInHandler; - m_impl->_focusOutCallback = initParams->FocusOutHandler; - m_impl->_maximizedCallback = initParams->MaximizedHandler; - m_impl->_minimizedCallback = initParams->MinimizedHandler; - m_impl->_restoredCallback = initParams->RestoredHandler; - m_impl->_customSchemeCallback = initParams->CustomSchemeHandler; - - for (int i = 0; i < 16; ++i) { - if (initParams->CustomSchemeNames[i] != nullptr) - m_impl->_customSchemeNames.emplace_back(initParams->CustomSchemeNames[i]); - } - - m_impl->_parent = initParams->ParentInstance; - - m_impl->_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - m_impl->_dialog = std::make_unique(); - - if (initParams->FullScreen) - SetFullScreen(true); - else { - if (initParams->Width > initParams->MaxWidth) - initParams->Width = initParams->MaxWidth; - if (initParams->Height > initParams->MaxHeight) - initParams->Height = initParams->MaxHeight; - if (initParams->Width < initParams->MinWidth) - initParams->Width = initParams->MinWidth; - if (initParams->Height < initParams->MinHeight) - initParams->Height = initParams->MinHeight; - - if (initParams->UseOsDefaultSize) - gtk_window_set_default_size(GTK_WINDOW(m_impl->_window), -1, -1); - else - gtk_window_set_default_size(GTK_WINDOW(m_impl->_window), initParams->Width, initParams->Height); - - SetMinSize(initParams->MinWidth, initParams->MinHeight); - SetMaxSize(initParams->MaxWidth, initParams->MaxHeight); - - if (initParams->UseOsDefaultLocation) - gtk_window_set_position(GTK_WINDOW(m_impl->_window), GTK_WIN_POS_NONE); - else if (initParams->CenterOnInitialize && !initParams->FullScreen) - gtk_window_set_position(GTK_WINDOW(m_impl->_window), GTK_WIN_POS_CENTER); - else - gtk_window_move(GTK_WINDOW(m_impl->_window), initParams->Left, initParams->Top); - } - - SetTitle(const_cast(m_impl->_windowTitle.c_str())); - - if (initParams->Chromeless) - gtk_window_set_decorated(GTK_WINDOW(m_impl->_window), false); - - if (initParams->WindowIconFile != nullptr && strlen(initParams->WindowIconFile) > 0) - SetIconFile(initParams->WindowIconFile); - - if (initParams->CenterOnInitialize) - Center(); - - if (initParams->Minimized) - SetMinimized(true); - - if (initParams->Maximized) - SetMaximized(true); - - if (!initParams->Resizable) - SetResizable(false); - - if (initParams->Topmost) - SetTopmost(true); - - g_signal_connect( - G_OBJECT(m_impl->_window), "configure-event", - G_CALLBACK(on_configure_event), this - ); - - g_signal_connect( - G_OBJECT(m_impl->_window), "window-state-event", - G_CALLBACK(on_window_state_event), this - ); - - g_signal_connect( - G_OBJECT(m_impl->_window), "delete-event", - G_CALLBACK(on_widget_deleted), this - ); - - g_signal_connect( - G_OBJECT(m_impl->_window), "destroy", - G_CALLBACK(on_widget_destroyed), this - ); - - // Register custom schemes before first navigation to avoid first-load races. - m_impl->AddCustomSchemeHandlers(); - - Show(false); - - g_signal_connect( - G_OBJECT(m_impl->_window), "focus-in-event", - G_CALLBACK(on_focus_in_event), this - ); - - g_signal_connect( - G_OBJECT(m_impl->_window), "focus-out-event", - G_CALLBACK(on_focus_out_event), this - ); - - g_signal_connect( - G_OBJECT(m_impl->_webview), "context-menu", - G_CALLBACK(on_webview_context_menu), this - ); - - g_signal_connect( - G_OBJECT(m_impl->_webview), "permission-request", - G_CALLBACK(on_permission_request), this - ); - - if (initParams->Transparent) - SetTransparentEnabled(true); - - if (m_impl->_zoom != 100.0) - SetZoom(m_impl->_zoom); -} - -InfiniFrameWindow::~InfiniFrameWindow() { - notify_uninit(); - gtk_widget_destroy(m_impl->_window); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Window Operations -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::Center() { - gint windowWidth, windowHeight; - gtk_window_get_size(GTK_WINDOW(m_impl->_window), &windowWidth, &windowHeight); - - GdkRectangle screen = {0}; - - GdkDisplay* d = gdk_display_get_default(); - if (d == nullptr) { - GtkWidget* dialog = gtk_message_dialog_new( - nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, - "gdk_display_get_default() returned NULL" - ); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - return; - } - - GdkMonitor* m = gdk_display_get_primary_monitor(d); - if (m == nullptr) { - m = gdk_display_get_monitor(d, 0); - if (m == nullptr) { - GtkWidget* dialog = gtk_message_dialog_new( - nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, - "gdk_display_get_primary_monitor() returned NULL" - ); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - return; - } - } - - gdk_monitor_get_geometry(m, &screen); - - gtk_window_move( - GTK_WINDOW(m_impl->_window), - (screen.width - windowWidth) / 2, - (screen.height - windowHeight) / 2 - ); -} - -void InfiniFrameWindow::ClearBrowserAutoFill() { - // TODO -} - -void InfiniFrameWindow::Close() { - gtk_window_close(GTK_WINDOW(m_impl->_window)); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Get Properties -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::GetTransparentEnabled(bool* enabled) const { - *enabled = m_impl->_transparentEnabled; -} - -void InfiniFrameWindow::GetContextMenuEnabled(bool* enabled) const { - *enabled = m_impl->_contextMenuEnabled; -} - -void InfiniFrameWindow::GetZoomEnabled(bool* enabled) const { - *enabled = m_impl->_zoomEnabled; -} - -void InfiniFrameWindow::GetDevToolsEnabled(bool* enabled) const { - WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_impl->_webview)); - *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; -} - -AutoString InfiniFrameWindow::GetUserAgent() const { - return AllocateStringCopy(m_impl->_userAgent); -} - -void InfiniFrameWindow::GetMediaAutoplayEnabled(bool* enabled) const { - *enabled = m_impl->_mediaAutoplayEnabled; -} - -void InfiniFrameWindow::GetFileSystemAccessEnabled(bool* enabled) const { - *enabled = m_impl->_fileSystemAccessEnabled; -} - -void InfiniFrameWindow::GetWebSecurityEnabled(bool* enabled) const { - *enabled = m_impl->_webSecurityEnabled; -} - -void InfiniFrameWindow::GetJavascriptClipboardAccessEnabled(bool* enabled) const { - *enabled = m_impl->_javascriptClipboardAccessEnabled; -} - -void InfiniFrameWindow::GetMediaStreamEnabled(bool* enabled) const { - *enabled = m_impl->_mediaStreamEnabled; -} - -void InfiniFrameWindow::GetSmoothScrollingEnabled(bool* enabled) const { - *enabled = m_impl->_smoothScrollingEnabled; -} - -void InfiniFrameWindow::GetIgnoreCertificateErrorsEnabled(bool* enabled) const { - *enabled = m_impl->_ignoreCertificateErrorsEnabled; -} - -void InfiniFrameWindow::GetMaximized(bool* isMaximized) const { - GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(m_impl->_window)); - GdkWindowState flags = gdk_window_get_state(gdk_window); - *isMaximized = flags & GDK_WINDOW_STATE_MAXIMIZED; -} - -void InfiniFrameWindow::GetMinimized(bool* isMinimized) const { - GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(m_impl->_window)); - GdkWindowState flags = gdk_window_get_state(gdk_window); - *isMinimized = flags & GDK_WINDOW_STATE_ICONIFIED; -} - -void InfiniFrameWindow::GetPosition(int* x, int* y) const { - gtk_window_get_position(GTK_WINDOW(m_impl->_window), x, y); -} - -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); -} - -void InfiniFrameWindow::GetMaxSize(int* width, int* height) const { - if (width) - *width = m_impl->_maxWidth; - if (height) - *height = m_impl->_maxHeight; -} - -void InfiniFrameWindow::GetMinSize(int* width, int* height) const { - if (width) - *width = m_impl->_minWidth; - if (height) - *height = m_impl->_minHeight; -} - -AutoString InfiniFrameWindow::GetTitle() const { - const char* title = gtk_window_get_title(GTK_WINDOW(m_impl->_window)); - return g_strdup(title ? title : ""); -} - -void InfiniFrameWindow::GetTopmost(bool* topmost) const { - GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(m_impl->_window)); - GdkWindowState flags = gdk_window_get_state(gdk_window); - *topmost = flags & GDK_WINDOW_STATE_ABOVE; -} - -void InfiniFrameWindow::GetZoom(int* zoom) const { - double rawValue = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(m_impl->_webview)); - rawValue = (rawValue * 100.0) + 0.5; - *zoom = static_cast(rawValue); -} - -void InfiniFrameWindow::GetFocused(bool* isFocused) const { - *isFocused = gtk_window_is_active(GTK_WINDOW(m_impl->_window)); -} - -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 -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::SetContextMenuEnabled(const bool enabled) { - m_impl->_contextMenuEnabled = enabled; -} - -void InfiniFrameWindow::SetZoomEnabled(bool enabled) { - // Not implemented on Linux -} - -void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { - m_impl->_devToolsEnabled = enabled; - WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_impl->_webview)); - 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); -} - -void InfiniFrameWindow::SetResizable(const bool resizable) { - gtk_window_set_resizable(GTK_WINDOW(m_impl->_window), resizable); -} - -void InfiniFrameWindow::SetMinSize(const int width, const int height) { - m_impl->_minWidth = width; - m_impl->_minHeight = height; - m_impl->_hints.min_width = width; - m_impl->_hints.min_height = height; - - gtk_window_set_geometry_hints( - GTK_WINDOW(m_impl->_window), - nullptr, - &m_impl->_hints, - (GdkWindowHints)(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE) - ); -} - -void InfiniFrameWindow::SetMaxSize(const int width, const int height) { - m_impl->_maxWidth = width; - m_impl->_maxHeight = height; - m_impl->_hints.max_width = width; - m_impl->_hints.max_height = height; - - gtk_window_set_geometry_hints( - GTK_WINDOW(m_impl->_window), - nullptr, - &m_impl->_hints, - (GdkWindowHints)(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE) - ); -} - -void InfiniFrameWindow::SetSize(const int width, const int height) { - gtk_window_resize(GTK_WINDOW(m_impl->_window), width, height); -} - -void InfiniFrameWindow::SetTitle(const AutoString title) { - gtk_window_set_title(GTK_WINDOW(m_impl->_window), title); -} - -void InfiniFrameWindow::SetTopmost(const bool topmost) { - gtk_window_set_keep_above(GTK_WINDOW(m_impl->_window), topmost); -} - -void InfiniFrameWindow::SetZoom(const int zoom) { - double newZoom = zoom / 100.0; - webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(m_impl->_webview), newZoom); -} - -void InfiniFrameWindow::SetFocused() { - gtk_window_present(GTK_WINDOW(m_impl->_window)); -} - -void InfiniFrameWindow::SetTransparentEnabled(const bool enabled) { - m_impl->_transparentEnabled = enabled; - - gtk_window_set_decorated(GTK_WINDOW(m_impl->_window), !enabled); - - GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_impl->_window)); - GdkVisual* rgba_visual = gdk_screen_get_rgba_visual(screen); - if (rgba_visual) { - gtk_widget_set_visual(GTK_WIDGET(m_impl->_window), rgba_visual); - gtk_widget_set_app_paintable(GTK_WIDGET(m_impl->_window), true); - - GdkRGBA color; - webkit_web_view_get_background_color(WEBKIT_WEB_VIEW(m_impl->_webview), &color); - color.alpha = enabled ? 0 : 1; - webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(m_impl->_webview), &color); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// 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( - G_OBJECT(m_impl->_window), "destroy", - G_CALLBACK( - +[](GtkWidget*, gpointer) { - gtk_main_quit(); - } - ), - nullptr - ); - gtk_main(); -} - -void InfiniFrameWindow::CloseWebView() { - // Not implemented on Linux -} - -// --------------------------------------------------------------------------------------------------------------------- -// Callbacks -// --------------------------------------------------------------------------------------------------------------------- - -InfiniFrameDialog* InfiniFrameWindow::GetDialog() const { - return m_impl->_dialog.get(); -} - -void InfiniFrameWindow::AddCustomSchemeName(const AutoStringConst scheme) { - if (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; -} - -void InfiniFrameWindow::SetClosedCallback(const ClosedCallback callback) { - m_impl->_closedCallback = callback; -} - -void InfiniFrameWindow::SetFocusInCallback(const FocusInCallback callback) { - m_impl->_focusInCallback = callback; -} - -void InfiniFrameWindow::SetFocusOutCallback(const FocusOutCallback callback) { - m_impl->_focusOutCallback = callback; -} - -void InfiniFrameWindow::SetMovedCallback(const MovedCallback callback) { - m_impl->_movedCallback = callback; -} - -void InfiniFrameWindow::SetResizedCallback(const ResizedCallback callback) { - m_impl->_resizedCallback = callback; -} - -void InfiniFrameWindow::SetMaximizedCallback(const MaximizedCallback callback) { - m_impl->_maximizedCallback = callback; -} - -void InfiniFrameWindow::SetRestoredCallback(const RestoredCallback callback) { - m_impl->_restoredCallback = callback; -} - -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(); - return false; -} - -void InfiniFrameWindow::InvokeClosed() const noexcept { - if (m_impl->_closedCallback) - m_impl->_closedCallback(); -} - -void InfiniFrameWindow::InvokeFocusIn() const noexcept { - if (m_impl->_focusInCallback) - m_impl->_focusInCallback(); -} - -void InfiniFrameWindow::InvokeFocusOut() const noexcept { - if (m_impl->_focusOutCallback) - m_impl->_focusOutCallback(); -} - -void InfiniFrameWindow::InvokeMove(int x, int y) const noexcept { - if (m_impl->_movedCallback) - m_impl->_movedCallback(x, y); -} - -void InfiniFrameWindow::InvokeResize(int width, int height) const noexcept { - if (m_impl->_resizedCallback) - m_impl->_resizedCallback(width, height); -} - -void InfiniFrameWindow::InvokeMaximized() const noexcept { - if (m_impl->_maximizedCallback) - m_impl->_maximizedCallback(); -} - -void InfiniFrameWindow::InvokeRestored() const noexcept { - if (m_impl->_restoredCallback) - m_impl->_restoredCallback(); -} - -void InfiniFrameWindow::InvokeMinimized() const noexcept { - if (m_impl->_minimizedCallback) - m_impl->_minimizedCallback(); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Private methods -// --------------------------------------------------------------------------------------------------------------------- - -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); - - m_impl->set_webkit_settings(); - - gtk_container_add(GTK_CONTAINER(m_impl->_window), m_impl->_webview); - gtk_widget_set_hexpand(m_impl->_webview, TRUE); - gtk_widget_set_vexpand(m_impl->_webview, TRUE); - - auto js = Embedded::InfiniFrameJsUtf8(); - - 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"); - - g_signal_connect( - G_OBJECT(m_impl->_webview), "load-changed", - G_CALLBACK(on_webview_load_changed), this - ); - g_signal_connect( - G_OBJECT(m_impl->_webview), "load-failed", - G_CALLBACK(on_webview_load_failed), this - ); - g_signal_connect( - G_OBJECT(m_impl->_webview), "web-process-terminated", - G_CALLBACK(on_webview_process_terminated), this - ); - g_signal_connect( - G_OBJECT(m_impl->_webview), "size-allocate", - G_CALLBACK(on_webview_size_allocate), this - ); - - 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); - } - - gtk_widget_show_all(m_impl->_window); -} - -void InfiniFrameWindow::AttachWebView() { - // On Linux, WebView is attached in Show() -} - -void InfiniFrameWindow::OnConfigureEvent(int x, int y, int width, int height) { - if (m_impl->_lastLeft != x || m_impl->_lastTop != y) { - InvokeMove(x, y); - m_impl->_lastLeft = x; - m_impl->_lastTop = y; - } - - if (m_impl->_lastHeight != height || m_impl->_lastWidth != width) { - InvokeResize(width, height); - m_impl->_lastWidth = width; - m_impl->_lastHeight = height; - } -} - -void InfiniFrameWindow::OnWindowStateEvent(GdkWindowState newState) { - if (newState & GDK_WINDOW_STATE_MAXIMIZED) { - InvokeMaximized(); - } - else if ((newState & GDK_WINDOW_STATE_ICONIFIED) || !gtk_widget_get_mapped(m_impl->_window)) { - InvokeMinimized(); - } - else if (!(newState & GDK_WINDOW_STATE_MAXIMIZED) && !(newState & GDK_WINDOW_STATE_ICONIFIED)) { - InvokeRestored(); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// GTK Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -gboolean on_configure_event(GtkWidget* widget, GdkEvent* event, const gpointer self) { - if (event->type == GDK_CONFIGURE) { - auto* instance = reinterpret_cast(self); - instance->OnConfigureEvent( - event->configure.x, event->configure.y, - event->configure.width, event->configure.height - ); - } - return FALSE; -} - -gboolean on_window_state_event(GtkWidget* widget, GdkEventWindowState* event, const gpointer self) { - auto* instance = reinterpret_cast(self); - instance->OnWindowStateEvent(event->new_window_state); - return TRUE; -} - -gboolean on_widget_deleted(GtkWidget* widget, GdkEvent* event, const gpointer self) { - auto* instance = reinterpret_cast(self); - return instance->InvokeClose(); -} - -void on_widget_destroyed(GtkWidget* widget, const gpointer self) { - auto* instance = reinterpret_cast(self); - instance->InvokeClosed(); -} - -gboolean on_focus_in_event(GtkWidget* widget, GdkEvent* event, const gpointer self) { - auto* instance = reinterpret_cast(self); - instance->InvokeFocusIn(); - return FALSE; -} - -gboolean on_focus_out_event(GtkWidget* widget, GdkEvent* event, const gpointer self) { - auto* instance = reinterpret_cast(self); - instance->InvokeFocusOut(); - return FALSE; -} - -gboolean on_webview_context_menu( - WebKitWebView* web_view, - GtkWidget* default_menu, - WebKitHitTestResult* hit_test_result, - gboolean triggered_with_keyboard, - const gpointer self - ) { - auto* instance = reinterpret_cast(self); - bool contextMenuEnabled = false; - instance->GetContextMenuEnabled(&contextMenuEnabled); - return !contextMenuEnabled; -} - -gboolean on_permission_request(WebKitWebView* web_view, WebKitPermissionRequest* request, gpointer user_data) { - auto* instance = reinterpret_cast(user_data); - bool grant = false; - instance->GetGrantBrowserPermissions(&grant); - if (grant) - webkit_permission_request_allow(request); - else - webkit_permission_request_deny(request); - return TRUE; -} - -void on_webview_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event, gpointer user_data) { - if (!linux_webview_diagnostics_enabled()) - return; - - const char* uri = webkit_web_view_get_uri(web_view); - g_message( - "[InfiniFrame/Linux] WebKit load-changed: event=%s uri=%s", - webkit_load_event_to_string(load_event), - uri ? uri : "" - ); -} - -gboolean on_webview_load_failed( - WebKitWebView* web_view, - WebKitLoadEvent load_event, - gchar* failing_uri, - GError* error, - gpointer user_data - ) { - if (!linux_webview_diagnostics_enabled()) - return FALSE; - - g_warning( - "[InfiniFrame/Linux] WebKit load-failed: event=%s uri=%s error=%s", - webkit_load_event_to_string(load_event), - failing_uri ? failing_uri : "", - error ? error->message : "" - ); - return FALSE; -} - -void on_webview_process_terminated( - WebKitWebView* web_view, - WebKitWebProcessTerminationReason reason, - gpointer user_data - ) { - g_warning( - "[InfiniFrame/Linux] WebKit web process terminated: reason=%s", - webkit_termination_reason_to_string(reason) - ); -} - -void on_webview_size_allocate(GtkWidget* widget, GtkAllocation* allocation, gpointer user_data) { - if (!linux_webview_diagnostics_enabled()) - return; - - g_message( - "[InfiniFrame/Linux] WebView size-allocate: %dx%d", - allocation ? allocation->width : -1, - allocation ? allocation->height : -1 - ); -} - -#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/UiDispatcher.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/UiDispatcher.Cocoa.mm new file mode 100644 index 000000000..6ef3bea97 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/UiDispatcher.Cocoa.mm @@ -0,0 +1,13 @@ +#ifdef __APPLE__ + +#include "../Window.Cocoa.Internal.h" + +void InfiniFrameWindow::Invoke(ACTION callback) +{ + if ([NSThread isMainThread]) + callback(); + else + dispatch_sync(dispatch_get_main_queue(), ^(void){ callback(); }); +} + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowCore.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowCore.Cocoa.mm new file mode 100644 index 000000000..b5a5e569f --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowCore.Cocoa.mm @@ -0,0 +1,300 @@ +#ifdef __APPLE__ + +#include + +#include "../AppDelegate.h" +#include "../../../Public/InfiniFrameDialog.h" +#include "../../../Public/InfiniFrameWindow.h" +#include "../NSWindowBorderless.h" +#include "../Window.Cocoa.Internal.h" +#include "../WindowDelegate.h" + +void InfiniFrameWindow::Register() +{ + [NSAutoreleasePool new]; + + AppDelegate *appDelegate = [[[AppDelegate alloc] init] autorelease]; + + NSApplication *application = [NSApplication sharedApplication]; + [application setDelegate: appDelegate]; + [application setActivationPolicy: NSApplicationActivationPolicyRegular]; + + NSString *appName = [[NSProcessInfo processInfo] processName]; + + NSMenu *mainMenu = [[NSMenu new] autorelease]; + NSMenuItem *mainMenuItem = [[NSMenuItem new] autorelease]; + [mainMenu addItem: mainMenuItem]; + + NSMenu *mainSubMenu = [[NSMenu new] autorelease]; + [mainMenuItem setSubmenu: mainSubMenu]; + + NSMenuItem *selectMenuItem = [[ + [NSMenuItem alloc] + initWithTitle: @"Select All" + action: @selector(selectAll:) + keyEquivalent: @"a" + ] autorelease]; + [mainSubMenu addItem: selectMenuItem]; + + NSMenuItem *cutMenuItem = [[ + [NSMenuItem alloc] + initWithTitle: @"Cut" + action: @selector(cut:) + keyEquivalent: @"x" + ] autorelease]; + [mainSubMenu addItem: cutMenuItem]; + + NSMenuItem *copyMenuItem = [[ + [NSMenuItem alloc] + initWithTitle: @"Copy" + action: @selector(copy:) + keyEquivalent: @"c" + ] autorelease]; + [mainSubMenu addItem: copyMenuItem]; + + NSMenuItem *pasteMenuItem = [[ + [NSMenuItem alloc] + initWithTitle: @"Paste" + action: @selector(paste:) + keyEquivalent: @"v" + ] autorelease]; + [mainSubMenu addItem: pasteMenuItem]; + + NSMenuItem *quitMenuItem = [[ + [NSMenuItem alloc] + initWithTitle: [@"Quit " stringByAppendingString: appName] + action: @selector(terminate:) + keyEquivalent: @"q" + ] autorelease]; + [mainSubMenu addItem: quitMenuItem]; + + [NSApp setMainMenu: mainMenu]; +} + +InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : m_impl(std::make_unique()) +{ + m_impl->_windowTitle = initParams->Title ? initParams->Title : ""; + + if (initParams->StartUrl != nullptr) + m_impl->_startUrl = initParams->StartUrl; + + if (initParams->StartString != nullptr) + m_impl->_startString = initParams->StartString; + + if (initParams->TemporaryFilesPath != nullptr) + m_impl->_temporaryFilesPath = initParams->TemporaryFilesPath; + + m_impl->_ignoreCertificateErrorsEnabled = initParams->IgnoreCertificateErrorsEnabled; + m_impl->_contextMenuEnabled = initParams->ContextMenuEnabled; + m_impl->_zoomEnabled = initParams->ZoomEnabled; + m_impl->_grantBrowserPermissions = initParams->GrantBrowserPermissions; + + m_impl->_webMessageReceivedCallback = initParams->WebMessageReceivedHandler; + m_impl->_resizedCallback = initParams->ResizedHandler; + m_impl->_movedCallback = initParams->MovedHandler; + m_impl->_closingCallback = initParams->ClosingHandler; + m_impl->_closedCallback = initParams->ClosedHandler; + m_impl->_focusInCallback = initParams->FocusInHandler; + m_impl->_focusOutCallback = initParams->FocusOutHandler; + m_impl->_maximizedCallback = initParams->MaximizedHandler; + m_impl->_minimizedCallback = initParams->MinimizedHandler; + m_impl->_restoredCallback = initParams->RestoredHandler; + m_impl->_customSchemeCallback = initParams->CustomSchemeHandler; + + for (int i = 0; i < 16; ++i) + { + if (initParams->CustomSchemeNames[i] != nullptr) + m_impl->_customSchemeNames.emplace_back(initParams->CustomSchemeNames[i]); + } + + m_impl->_parent = initParams->ParentInstance; + + if (initParams->UseOsDefaultSize) + { + initParams->Width = 800; + initParams->Height = 600; + } + else + { + if (initParams->Width < 0) initParams->Width = 800; + if (initParams->Height < 0) initParams->Height = 600; + } + + if (initParams->UseOsDefaultLocation) + { + initParams->Left = 0; + initParams->Top = 0; + } + + NSRect frame = NSMakeRect(0, 0, 0, 0); + + m_impl->_chromeless = initParams->Chromeless; + if (initParams->Chromeless) + { + m_impl->_window = [[NSWindowBorderless alloc] + initWithContentRect: frame + styleMask: NSWindowStyleMaskBorderless + | NSWindowStyleMaskClosable + | NSWindowStyleMaskResizable + | NSWindowStyleMaskMiniaturizable + backing: NSBackingStoreBuffered + defer: true]; + } + else + { + m_impl->_window = [[NSWindow alloc] + initWithContentRect: frame + styleMask: NSWindowStyleMaskTitled + | NSWindowStyleMaskClosable + | NSWindowStyleMaskResizable + | NSWindowStyleMaskMiniaturizable + backing: NSBackingStoreBuffered + defer: true]; + } + + m_impl->_transparentEnabled = initParams->Transparent; + + if (m_impl->_parent != nullptr && m_impl->_parent->m_impl != nullptr) + { + auto* parentImpl = static_cast(m_impl->_parent->m_impl.get()); + m_impl->_nativeParentWindow = parentImpl->_window; + if (m_impl->_nativeParentWindow != nil && m_impl->_nativeParentWindow != m_impl->_window) + { + [m_impl->_nativeParentWindow addChildWindow:m_impl->_window ordered:NSWindowAbove]; + + NSWindow* childWindow = m_impl->_window; + m_impl->_parentWillCloseObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:NSWindowWillCloseNotification + object:m_impl->_nativeParentWindow + queue:nil + usingBlock:^(NSNotification*) { + [childWindow close]; + }]; + } + } + + [m_impl->_window setCollectionBehavior: + [m_impl->_window collectionBehavior] | NSWindowCollectionBehaviorFullScreenPrimary]; + + WindowDelegate *windowDelegate = [WindowDelegate new]; + windowDelegate->infiniFrame = this; + m_impl->_window.delegate = windowDelegate; + + SetTitle(const_cast(m_impl->_windowTitle.c_str())); + + if (initParams->WindowIconFile != nullptr && initParams->WindowIconFile[0] != '\0') + SetIconFile(initParams->WindowIconFile); + + SetTopmost(initParams->Topmost); + SetPosition(initParams->Left, initParams->Top); + + SetMinSize(initParams->MinWidth, initParams->MinHeight); + SetMaxSize(initParams->MaxWidth, initParams->MaxHeight); + SetSize(initParams->Width, initParams->Height); + + SetMinimized(initParams->Minimized); + SetMaximized(initParams->Maximized); + SetResizable(initParams->Resizable); + + if (initParams->CenterOnInitialize) + Center(); + + m_impl->_webviewConfiguration = [[WKWebViewConfiguration alloc] init]; + + for (const auto & scheme : m_impl->_customSchemeNames) + { + m_impl->AddCustomScheme(scheme.c_str(), m_impl->_customSchemeCallback); + } + + 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->_dialog = std::make_unique(); + + Show(false); + SetFullScreen(initParams->FullScreen); +} + +InfiniFrameWindow::~InfiniFrameWindow() +{ + if (m_impl->_parentWillCloseObserver != nil) { + [[NSNotificationCenter defaultCenter] removeObserver:m_impl->_parentWillCloseObserver]; + m_impl->_parentWillCloseObserver = nil; + } + + if (m_impl->_nativeParentWindow != nil && m_impl->_window != nil) { + [m_impl->_nativeParentWindow removeChildWindow:m_impl->_window]; + m_impl->_nativeParentWindow = nil; + } + + [m_impl->_webviewConfiguration release]; + [m_impl->_webview release]; + [m_impl->_window performClose: m_impl->_window]; +} + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowEvents.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowEvents.Cocoa.mm new file mode 100644 index 000000000..ee92e136b --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowEvents.Cocoa.mm @@ -0,0 +1,143 @@ +#ifdef __APPLE__ + +#include "../Window.Cocoa.Internal.h" + +InfiniFrameDialog* InfiniFrameWindow::GetDialog() const +{ + return m_impl->_dialog.get(); +} + +void InfiniFrameWindow::AddCustomSchemeName(const AutoStringConst scheme) +{ + if (scheme) + m_impl->_customSchemeNames.emplace_back(scheme); +} + +void InfiniFrameWindow::GetAllMonitors(GetAllMonitorsCallback callback) const +{ + if (callback) + { + 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); + } + } +} + +void InfiniFrameWindow::SetClosingCallback(const ClosingCallback callback) +{ + m_impl->_closingCallback = callback; +} + +void InfiniFrameWindow::SetClosedCallback(const ClosedCallback callback) +{ + m_impl->_closedCallback = callback; +} + +void InfiniFrameWindow::SetFocusInCallback(const FocusInCallback callback) +{ + m_impl->_focusInCallback = callback; +} + +void InfiniFrameWindow::SetFocusOutCallback(const FocusOutCallback callback) +{ + m_impl->_focusOutCallback = callback; +} + +void InfiniFrameWindow::SetMovedCallback(const MovedCallback callback) +{ + m_impl->_movedCallback = callback; +} + +void InfiniFrameWindow::SetResizedCallback(const ResizedCallback callback) +{ + m_impl->_resizedCallback = callback; +} + +void InfiniFrameWindow::SetMaximizedCallback(const MaximizedCallback callback) +{ + m_impl->_maximizedCallback = callback; +} + +void InfiniFrameWindow::SetRestoredCallback(const RestoredCallback callback) +{ + m_impl->_restoredCallback = callback; +} + +void InfiniFrameWindow::SetMinimizedCallback(const MinimizedCallback callback) +{ + m_impl->_minimizedCallback = callback; +} + +[[nodiscard]] bool InfiniFrameWindow::InvokeClose() const noexcept +{ + if (m_impl->_closingCallback) + return m_impl->_closingCallback(); + return false; +} + +void InfiniFrameWindow::InvokeClosed() const noexcept +{ + if (m_impl->_closedCallback) + m_impl->_closedCallback(); +} + +void InfiniFrameWindow::InvokeFocusIn() const noexcept +{ + if (m_impl->_focusInCallback) + m_impl->_focusInCallback(); +} + +void InfiniFrameWindow::InvokeFocusOut() const noexcept +{ + if (m_impl->_focusOutCallback) + m_impl->_focusOutCallback(); +} + +void InfiniFrameWindow::InvokeMove(int x, int y) const noexcept +{ + if (m_impl->_movedCallback) + m_impl->_movedCallback(x, y); +} + +void InfiniFrameWindow::InvokeResize(int width, int height) const noexcept +{ + if (m_impl->_resizedCallback) + m_impl->_resizedCallback(width, height); +} + +void InfiniFrameWindow::InvokeMaximized() const noexcept +{ + if (m_impl->_maximizedCallback) + m_impl->_maximizedCallback(); +} + +void InfiniFrameWindow::InvokeRestored() const noexcept +{ + if (m_impl->_restoredCallback) + m_impl->_restoredCallback(); +} + +void InfiniFrameWindow::InvokeMinimized() const noexcept +{ + if (m_impl->_minimizedCallback) + m_impl->_minimizedCallback(); +} + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowLifecycle.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowLifecycle.Cocoa.mm new file mode 100644 index 000000000..8bb671e17 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowLifecycle.Cocoa.mm @@ -0,0 +1,79 @@ +#ifdef __APPLE__ + +#include "../Window.Cocoa.Internal.h" + +void InfiniFrameWindow::Center() +{ + [m_impl->_window center]; + [m_impl->_window makeKeyAndOrderFront: m_impl->_window]; +} + +void InfiniFrameWindow::ClearBrowserAutoFill() +{ + // TODO +} + +void InfiniFrameWindow::Close() +{ + if (m_impl->_parentWillCloseObserver != nil) { + [[NSNotificationCenter defaultCenter] removeObserver:m_impl->_parentWillCloseObserver]; + m_impl->_parentWillCloseObserver = nil; + } + + if (m_impl->_nativeParentWindow != nil && m_impl->_window != nil) { + [m_impl->_nativeParentWindow removeChildWindow:m_impl->_window]; + m_impl->_nativeParentWindow = nil; + } + + if (m_impl->_chromeless) + [m_impl->_window close]; + else + [m_impl->_window performClose: m_impl->_window]; +} + +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)error; + }]; +} + +void InfiniFrameWindow::WaitForExit() +{ + if (![NSApp isRunning]) { + [NSApp run]; + return; + } + + __block bool windowClosed = false; + id observer = [[NSNotificationCenter defaultCenter] + addObserverForName: NSWindowWillCloseNotification + object: m_impl->_window + queue: nil + usingBlock: ^(NSNotification*) { + windowClosed = true; + }]; + + while (!windowClosed) { + [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode + beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.05]]; + } + + [[NSNotificationCenter defaultCenter] removeObserver: observer]; +} + +void InfiniFrameWindow::CloseWebView() +{ + // Not implemented on macOS +} + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowState.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowState.Cocoa.mm new file mode 100644 index 000000000..a72a33cef --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Core/WindowState.Cocoa.mm @@ -0,0 +1,383 @@ +#ifdef __APPLE__ + +#include "../Window.Cocoa.Internal.h" + +#include "Utils/Common.h" + +static const int MAX_WINDOW_DIMENSION = 10000; + +void InfiniFrameWindow::GetTransparentEnabled(bool* enabled) const +{ + *enabled = false; +} + +void InfiniFrameWindow::GetContextMenuEnabled(bool* enabled) const +{ + *enabled = m_impl->_contextMenuEnabled; +} + +void InfiniFrameWindow::GetZoomEnabled(bool* enabled) const +{ + *enabled = m_impl->_zoomEnabled; +} + +void InfiniFrameWindow::GetDevToolsEnabled(bool* enabled) const +{ + *enabled = m_impl->_devToolsEnabled; +} + +void InfiniFrameWindow::GetGrantBrowserPermissions(bool* enabled) const +{ + *enabled = m_impl->_grantBrowserPermissions; +} + +AutoString InfiniFrameWindow::GetUserAgent() const +{ + return AllocateStringCopy(m_impl->_userAgent); +} + +void InfiniFrameWindow::GetMediaAutoplayEnabled(bool* enabled) const +{ + *enabled = true; +} + +void InfiniFrameWindow::GetFileSystemAccessEnabled(bool* enabled) const +{ + *enabled = m_impl->_fileSystemAccessEnabled; +} + +void InfiniFrameWindow::GetSmoothScrollingEnabled(bool* enabled) const +{ + *enabled = false; +} + +void InfiniFrameWindow::GetWebSecurityEnabled(bool* enabled) const +{ + *enabled = m_impl->_webSecurityEnabled; +} + +void InfiniFrameWindow::GetJavascriptClipboardAccessEnabled(bool* enabled) const +{ + *enabled = m_impl->_javascriptClipboardAccessEnabled; +} + +void InfiniFrameWindow::GetMediaStreamEnabled(bool* enabled) const +{ + *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]; + NSScreen* screen = [m_impl->_window screen]; + if (!screen) screen = [NSScreen mainScreen]; + NSRect screenFrame = [screen frame]; + int height = static_cast(roundf(frame.size.height)); + *x = static_cast(roundf(frame.origin.x)); + *y = static_cast(roundf(screenFrame.origin.y + screenFrame.size.height - (frame.origin.y + height))); +} + +void InfiniFrameWindow::GetResizable(bool* resizable) const +{ + *resizable = (([m_impl->_window styleMask] & NSWindowStyleMaskResizable) == NSWindowStyleMaskResizable); +} + +void InfiniFrameWindow::GetIgnoreCertificateErrorsEnabled(bool* enabled) const +{ + *enabled = m_impl->_ignoreCertificateErrorsEnabled; +} + +void InfiniFrameWindow::GetFocused(bool* isFocused) const +{ + if (!isFocused) + return; + + if (!m_impl->_window) + { + *isFocused = false; + return; + } + + *isFocused = [NSApp isActive] && [m_impl->_window isKeyWindow]; +} + +unsigned int InfiniFrameWindow::GetScreenDpi() const +{ + return 72; +} + +void InfiniFrameWindow::GetSize(int* width, int* height) const +{ + NSSize size = [m_impl->_window frame].size; + if (width) *width = static_cast(roundf(size.width)); + if (height) *height = static_cast(roundf(size.height)); +} + +void InfiniFrameWindow::GetMaxSize(int* width, int* height) const +{ + NSSize maxSize = [m_impl->_window maxSize]; + if (width) *width = static_cast(roundf(maxSize.width)); + if (height) *height = static_cast(roundf(maxSize.height)); +} + +void InfiniFrameWindow::GetMinSize(int* width, int* height) const +{ + NSSize minSize = [m_impl->_window minSize]; + if (width) *width = static_cast(roundf(minSize.width)); + if (height) *height = static_cast(roundf(minSize.height)); +} + +AutoString InfiniFrameWindow::GetTitle() const +{ + return AllocateStringCopy(m_impl->_windowTitle); +} + +void InfiniFrameWindow::GetTopmost(bool* topmost) const +{ + *topmost = ([m_impl->_window level] & NSFloatingWindowLevel) == NSFloatingWindowLevel; +} + +void InfiniFrameWindow::GetZoom(int* zoom) const +{ + CGFloat rawValue = [m_impl->_webview magnification]; + rawValue = (rawValue * 100.0) + 0.5; + *zoom = static_cast(rawValue); +} + +AutoString InfiniFrameWindow::GetIconFileName() const +{ + return AllocateStringCopy(m_impl->_iconFileName); +} + +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]; +} + +void InfiniFrameWindow::SetDevToolsEnabled(bool enabled) +{ + m_impl->_devToolsEnabled = enabled; + m_impl->SetPreference(@"developerExtrasEnabled", enabled ? @YES : @NO); +} + +void InfiniFrameWindow::SetTransparentEnabled(bool enabled) +{ + (void)enabled; +} + +void InfiniFrameWindow::SetContextMenuEnabled(bool enabled) +{ + (void)enabled; +} + +void InfiniFrameWindow::SetZoomEnabled(bool enabled) +{ + (void)enabled; +} + +void InfiniFrameWindow::SetIconFile(AutoString filename) +{ + NSString* path = [NSString stringWithUTF8String: filename]; + NSImage* icon = [[NSImage alloc] initWithContentsOfFile: path]; + if (icon != nil) + [[m_impl->_window standardWindowButton: NSWindowDocumentIconButton] setImage: icon]; + + 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]; + if (!screen) screen = [NSScreen mainScreen]; + NSRect screenFrame = [screen frame]; + + NSRect frame = [m_impl->_window frame]; + int height = static_cast(roundf(frame.size.height)); + + auto left = static_cast(x); + auto top = static_cast(screenFrame.origin.y + screenFrame.size.height - (y + height)); + + [m_impl->_window setFrameOrigin: CGPointMake(left, top)]; +} + +void InfiniFrameWindow::SetResizable(bool resizable) +{ + if (resizable) + m_impl->_window.styleMask |= NSWindowStyleMaskResizable; + else + m_impl->_window.styleMask &= ~NSWindowStyleMaskResizable; +} + +void InfiniFrameWindow::SetSize(int width, int height) +{ + width = width > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : width; + height = height > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : height; + + if (width > m_impl->_window.maxSize.width) width = m_impl->_window.maxSize.width; + if (height > m_impl->_window.maxSize.height) height = m_impl->_window.maxSize.height; + if (width < m_impl->_window.minSize.width) width = m_impl->_window.minSize.width; + if (height < m_impl->_window.minSize.height) height = m_impl->_window.minSize.height; + + NSRect frame = [m_impl->_window frame]; + CGFloat oldHeight = frame.size.height; + frame.size = CGSizeMake(static_cast(width), static_cast(height)); + frame.origin.y -= static_cast(height) - oldHeight; + + [m_impl->_window setFrame: frame display: true]; +} + +void InfiniFrameWindow::SetMinSize(int width, int height) +{ + width = width > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : width; + height = height > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : height; + + [m_impl->_window setMinSize: NSMakeSize(width, height)]; +} + +void InfiniFrameWindow::SetMaxSize(int width, int height) +{ + width = width > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : width; + height = height > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : height; + + [m_impl->_window setMaxSize: NSMakeSize(width, height)]; +} + +void InfiniFrameWindow::SetTitle(AutoString title) +{ + m_impl->_windowTitle = title ? title : ""; + [m_impl->_window setTitle: [NSString stringWithUTF8String: title]]; +} + +void InfiniFrameWindow::SetTopmost(bool topmost) +{ + if (topmost) [m_impl->_window setLevel: NSFloatingWindowLevel]; + else [m_impl->_window setLevel: NSNormalWindowLevel]; +} + +void InfiniFrameWindow::SetZoom(int zoom) +{ + CGFloat newZoom = zoom / 100.0; + [m_impl->_webview setMagnification: newZoom]; +} + +void InfiniFrameWindow::SetFocused() +{ + if (!m_impl->_window) return; + + [NSApp activateIgnoringOtherApps: YES]; + [m_impl->_window makeKeyAndOrderFront: m_impl->_window]; + + if (![m_impl->_window isKeyWindow]) + { + [m_impl->_window orderFrontRegardless]; + [m_impl->_window makeKeyWindow]; + } +} + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Dialog.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Dialog.mm index 80144d07b..09ac5845b 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Dialog.mm +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Dialog.mm @@ -4,7 +4,7 @@ * @brief macOS implementation of InfiniFrameDialog using NSOpenPanel, NSSavePanel, and NSAlert */ -#import "Core/InfiniFrameDialog.h" +#import "Public/InfiniFrameDialog.h" #if defined(VSTGUI_USE_OBJC_UTTYPE) #import diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/NSWindowBorderless.h b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/NSWindowBorderless.h index fae41e0bb..4aebdb279 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/NSWindowBorderless.h +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/NSWindowBorderless.h @@ -7,7 +7,7 @@ * Used when InfiniFrameInitParams::Transparent is set, allowing the WebView to render * over a fully transparent window background without the standard title bar and borders */ -#include "Core/InfiniFrame.h" +#include "Public/InfiniFrame.h" /** * @brief Borderless, transparent NSWindow subclass. diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/NavigationDelegate.h b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/NavigationDelegate.h index 2d638074e..d8ee80f75 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/NavigationDelegate.h +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/NavigationDelegate.h @@ -4,7 +4,7 @@ * @file NavigationDelegate.h * @brief WKNavigationDelegate that handles TLS certificate validation for the embedded WebView */ -#include "Core/InfiniFrame.h" +#include "Public/InfiniFrame.h" /** * @brief Navigation delegate conforming to WKNavigationDelegate. diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UiDelegate.h b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UiDelegate.h index 3941068c1..1c5768891 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UiDelegate.h +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UiDelegate.h @@ -4,7 +4,7 @@ * @file UiDelegate.h * @brief WKUIDelegate and WKScriptMessageHandler that routes JavaScript messages to the .NET layer */ -#include "Core/InfiniFrame.h" +#include "Public/InfiniFrame.h" /** * @brief UI delegate conforming to WKUIDelegate and WKScriptMessageHandler. diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UiDelegate.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UiDelegate.mm index 525cd055d..0e0ab8023 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UiDelegate.mm +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UiDelegate.mm @@ -35,6 +35,7 @@ - (void)webView:(WKWebView *)webView [alert addButtonWithTitle:@"OK"]; [alert beginSheetModalForWindow:window completionHandler:^void (NSModalResponse response) { + (void)response; completionHandler(); [alert release]; }]; diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UrlSchemeHandler.h b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UrlSchemeHandler.h index 9d5f201f0..0639b7600 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UrlSchemeHandler.h +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/UrlSchemeHandler.h @@ -4,7 +4,7 @@ * @file UrlSchemeHandler.h * @brief WKURLSchemeHandler that intercepts custom-scheme requests and serves responses from the .NET layer */ -#include "Core/InfiniFrame.h" +#include "Public/InfiniFrame.h" /** * @brief URL scheme handler conforming to WKURLSchemeHandler. diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitBridge.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitBridge.Cocoa.mm new file mode 100644 index 000000000..1a2b8f684 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitBridge.Cocoa.mm @@ -0,0 +1,57 @@ +#ifdef __APPLE__ + +#include + +#include "../Window.Cocoa.Internal.h" + +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]; +} + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitCustomSchemes.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitCustomSchemes.Cocoa.mm new file mode 100644 index 000000000..e7401b90c --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitCustomSchemes.Cocoa.mm @@ -0,0 +1,22 @@ +#ifdef __APPLE__ + +#include "../UrlSchemeHandler.h" +#include "../Window.Cocoa.Internal.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]]; +} + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitHost.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitHost.Cocoa.mm new file mode 100644 index 000000000..e06e6f23a --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WebKit/WebKitHost.Cocoa.mm @@ -0,0 +1,71 @@ +#ifdef __APPLE__ + +#include "../../../Embedded/Embedded.h" +#include "../NavigationDelegate.h" +#include "../UiDelegate.h" +#include "../Window.Cocoa.Internal.h" + +void InfiniFrameWindow::AttachWebView() +{ + auto js = Embedded::InfiniFrameJsUtf8(); + + 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(); + + [m_impl->_window makeKeyAndOrderFront: m_impl->_window]; + [m_impl->_window orderFrontRegardless]; +} + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Window.Cocoa.Internal.h b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Window.Cocoa.Internal.h new file mode 100644 index 000000000..0bfc61030 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Window.Cocoa.Internal.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include +#include +#include + +#include "Public/InfiniFrameWindow.h" +#include "Public/InfiniFrameWindowImpl.h" + +struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { + NSWindow* _window = nil; + WKWebView* _webview = nil; + WKWebViewConfiguration* _webviewConfiguration = nil; + NSWindow* _nativeParentWindow = nil; + id _parentWillCloseObserver = 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); +}; diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Window.mm b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Window.mm deleted file mode 100644 index d55bb721d..000000000 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/Window.mm +++ /dev/null @@ -1,1097 +0,0 @@ -#ifdef __APPLE__ -#include "Core/InfiniFrameWindow.h" -#include "Core/InfiniFrameDialog.h" -#include "Core/InfiniFrameWindowImpl.h" -#include "Embedded/Embedded.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; - -static const int MAX_WINDOW_DIMENSION = 10000; - -// --------------------------------------------------------------------------------------------------------------------- -// Platform Impl -// --------------------------------------------------------------------------------------------------------------------- - -struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl -{ - NSWindow* _window = nil; - WKWebView* _webview = nil; - WKWebViewConfiguration* _webviewConfiguration = nil; - NSWindow* _nativeParentWindow = nil; - id _parentWillCloseObserver = 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) -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::Register() -{ - [NSAutoreleasePool new]; - - AppDelegate *appDelegate = [[[AppDelegate alloc] init] autorelease]; - - NSApplication *application = [NSApplication sharedApplication]; - [application setDelegate: appDelegate]; - [application setActivationPolicy: NSApplicationActivationPolicyRegular]; - - NSString *appName = [[NSProcessInfo processInfo] processName]; - - NSMenu *mainMenu = [[NSMenu new] autorelease]; - NSMenuItem *mainMenuItem = [[NSMenuItem new] autorelease]; - [mainMenu addItem: mainMenuItem]; - - NSMenu *mainSubMenu = [[NSMenu new] autorelease]; - [mainMenuItem setSubmenu: mainSubMenu]; - - NSMenuItem *selectMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: @"Select All" - action: @selector(selectAll:) - keyEquivalent: @"a" - ] autorelease]; - [mainSubMenu addItem: selectMenuItem]; - - NSMenuItem *cutMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: @"Cut" - action: @selector(cut:) - keyEquivalent: @"x" - ] autorelease]; - [mainSubMenu addItem: cutMenuItem]; - - NSMenuItem *copyMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: @"Copy" - action: @selector(copy:) - keyEquivalent: @"c" - ] autorelease]; - [mainSubMenu addItem: copyMenuItem]; - - NSMenuItem *pasteMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: @"Paste" - action: @selector(paste:) - keyEquivalent: @"v" - ] autorelease]; - [mainSubMenu addItem: pasteMenuItem]; - - NSMenuItem *quitMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: [@"Quit " stringByAppendingString: appName] - action: @selector(terminate:) - keyEquivalent: @"q" - ] autorelease]; - [mainSubMenu addItem: quitMenuItem]; - - [NSApp setMainMenu: mainMenu]; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Constructor / Destructor -// --------------------------------------------------------------------------------------------------------------------- - -InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : m_impl(std::make_unique()) -{ - m_impl->_windowTitle = initParams->Title ? initParams->Title : ""; - - if (initParams->StartUrl != nullptr) - m_impl->_startUrl = initParams->StartUrl; - - if (initParams->StartString != nullptr) - m_impl->_startString = initParams->StartString; - - if (initParams->TemporaryFilesPath != nullptr) - m_impl->_temporaryFilesPath = initParams->TemporaryFilesPath; - - m_impl->_ignoreCertificateErrorsEnabled = initParams->IgnoreCertificateErrorsEnabled; - m_impl->_contextMenuEnabled = initParams->ContextMenuEnabled; - m_impl->_zoomEnabled = initParams->ZoomEnabled; - m_impl->_grantBrowserPermissions = initParams->GrantBrowserPermissions; - - m_impl->_webMessageReceivedCallback = initParams->WebMessageReceivedHandler; - m_impl->_resizedCallback = initParams->ResizedHandler; - m_impl->_movedCallback = initParams->MovedHandler; - m_impl->_closingCallback = initParams->ClosingHandler; - m_impl->_closedCallback = initParams->ClosedHandler; - m_impl->_focusInCallback = initParams->FocusInHandler; - m_impl->_focusOutCallback = initParams->FocusOutHandler; - m_impl->_maximizedCallback = initParams->MaximizedHandler; - m_impl->_minimizedCallback = initParams->MinimizedHandler; - m_impl->_restoredCallback = initParams->RestoredHandler; - m_impl->_customSchemeCallback = initParams->CustomSchemeHandler; - - for (int i = 0; i < 16; ++i) - { - if (initParams->CustomSchemeNames[i] != nullptr) - m_impl->_customSchemeNames.emplace_back(initParams->CustomSchemeNames[i]); - } - - m_impl->_parent = initParams->ParentInstance; - - if (initParams->UseOsDefaultSize) - { - initParams->Width = 800; - initParams->Height = 600; - } - else - { - if (initParams->Width < 0) initParams->Width = 800; - if (initParams->Height < 0) initParams->Height = 600; - } - - if (initParams->UseOsDefaultLocation) - { - initParams->Left = 0; - initParams->Top = 0; - } - - NSRect frame = NSMakeRect(0, 0, 0, 0); - - m_impl->_chromeless = initParams->Chromeless; - if (initParams->Chromeless) - { - m_impl->_window = [[NSWindowBorderless alloc] - initWithContentRect: frame - styleMask: NSWindowStyleMaskBorderless - | NSWindowStyleMaskClosable - | NSWindowStyleMaskResizable - | NSWindowStyleMaskMiniaturizable - backing: NSBackingStoreBuffered - defer: true]; - } - else - { - m_impl->_window = [[NSWindow alloc] - initWithContentRect: frame - styleMask: NSWindowStyleMaskTitled - | NSWindowStyleMaskClosable - | NSWindowStyleMaskResizable - | NSWindowStyleMaskMiniaturizable - backing: NSBackingStoreBuffered - defer: true]; - } - - m_impl->_transparentEnabled = initParams->Transparent; - - if (m_impl->_parent != nullptr && m_impl->_parent->m_impl != nullptr) - { - auto* parentImpl = static_cast(m_impl->_parent->m_impl.get()); - m_impl->_nativeParentWindow = parentImpl->_window; - if (m_impl->_nativeParentWindow != nil && m_impl->_nativeParentWindow != m_impl->_window) - { - [m_impl->_nativeParentWindow addChildWindow:m_impl->_window ordered:NSWindowAbove]; - - NSWindow* childWindow = m_impl->_window; - m_impl->_parentWillCloseObserver = [[NSNotificationCenter defaultCenter] - addObserverForName:NSWindowWillCloseNotification - object:m_impl->_nativeParentWindow - queue:nil - usingBlock:^(NSNotification*) { - [childWindow close]; - }]; - } - } - - [m_impl->_window setCollectionBehavior: - [m_impl->_window collectionBehavior] | NSWindowCollectionBehaviorFullScreenPrimary]; - - WindowDelegate *windowDelegate = [WindowDelegate new]; - windowDelegate->infiniFrame = this; - m_impl->_window.delegate = windowDelegate; - - SetTitle(const_cast(m_impl->_windowTitle.c_str())); - - if (initParams->WindowIconFile != nullptr && initParams->WindowIconFile[0] != '\0') - SetIconFile(initParams->WindowIconFile); - - SetTopmost(initParams->Topmost); - SetPosition(initParams->Left, initParams->Top); - - SetMinSize(initParams->MinWidth, initParams->MinHeight); - SetMaxSize(initParams->MaxWidth, initParams->MaxHeight); - SetSize(initParams->Width, initParams->Height); - - SetMinimized(initParams->Minimized); - SetMaximized(initParams->Maximized); - SetResizable(initParams->Resizable); - - if (initParams->CenterOnInitialize) - 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); - } - - 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->_dialog = std::make_unique(); - - Show(false); - SetFullScreen(initParams->FullScreen); -} - -InfiniFrameWindow::~InfiniFrameWindow() -{ - if (m_impl->_parentWillCloseObserver != nil) { - [[NSNotificationCenter defaultCenter] removeObserver:m_impl->_parentWillCloseObserver]; - m_impl->_parentWillCloseObserver = nil; - } - - if (m_impl->_nativeParentWindow != nil && m_impl->_window != nil) { - [m_impl->_nativeParentWindow removeChildWindow:m_impl->_window]; - m_impl->_nativeParentWindow = nil; - } - - [m_impl->_webviewConfiguration release]; - [m_impl->_webview release]; - [m_impl->_window performClose: m_impl->_window]; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Window Operations -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::Center() -{ - [m_impl->_window center]; - [m_impl->_window makeKeyAndOrderFront: m_impl->_window]; -} - -void InfiniFrameWindow::ClearBrowserAutoFill() -{ - // TODO -} - -void InfiniFrameWindow::Close() -{ - if (m_impl->_parentWillCloseObserver != nil) { - [[NSNotificationCenter defaultCenter] removeObserver:m_impl->_parentWillCloseObserver]; - m_impl->_parentWillCloseObserver = nil; - } - - if (m_impl->_nativeParentWindow != nil && m_impl->_window != nil) { - [m_impl->_nativeParentWindow removeChildWindow:m_impl->_window]; - m_impl->_nativeParentWindow = nil; - } - - if (m_impl->_chromeless) - [m_impl->_window close]; - else - [m_impl->_window performClose: m_impl->_window]; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Get Properties -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::GetTransparentEnabled(bool* enabled) const -{ - *enabled = false; -} - -void InfiniFrameWindow::GetContextMenuEnabled(bool* enabled) const -{ - *enabled = m_impl->_contextMenuEnabled; -} - -void InfiniFrameWindow::GetZoomEnabled(bool* enabled) const -{ - *enabled = m_impl->_zoomEnabled; -} - -void InfiniFrameWindow::GetDevToolsEnabled(bool* enabled) const -{ - *enabled = m_impl->_devToolsEnabled; -} - -void InfiniFrameWindow::GetGrantBrowserPermissions(bool* enabled) const -{ - *enabled = m_impl->_grantBrowserPermissions; -} - -AutoString InfiniFrameWindow::GetUserAgent() const -{ - return AllocateStringCopy(m_impl->_userAgent); -} - -void InfiniFrameWindow::GetMediaAutoplayEnabled(bool* enabled) const -{ - *enabled = true; -} - -void InfiniFrameWindow::GetFileSystemAccessEnabled(bool* enabled) const -{ - *enabled = m_impl->_fileSystemAccessEnabled; -} - -void InfiniFrameWindow::GetSmoothScrollingEnabled(bool* enabled) const -{ - *enabled = false; -} - -void InfiniFrameWindow::GetWebSecurityEnabled(bool* enabled) const -{ - *enabled = m_impl->_webSecurityEnabled; -} - -void InfiniFrameWindow::GetJavascriptClipboardAccessEnabled(bool* enabled) const -{ - *enabled = m_impl->_javascriptClipboardAccessEnabled; -} - -void InfiniFrameWindow::GetMediaStreamEnabled(bool* enabled) const -{ - *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]; - NSScreen* screen = [m_impl->_window screen]; - if (!screen) screen = [NSScreen mainScreen]; - NSRect screenFrame = [screen frame]; - int height = static_cast(roundf(frame.size.height)); - *x = static_cast(roundf(frame.origin.x)); - *y = static_cast(roundf(screenFrame.origin.y + screenFrame.size.height - (frame.origin.y + height))); -} - -void InfiniFrameWindow::GetResizable(bool* resizable) const -{ - *resizable = (([m_impl->_window styleMask] & NSWindowStyleMaskResizable) == NSWindowStyleMaskResizable); -} - -void InfiniFrameWindow::GetIgnoreCertificateErrorsEnabled(bool* enabled) const -{ - *enabled = m_impl->_ignoreCertificateErrorsEnabled; -} - -void InfiniFrameWindow::GetFocused(bool* isFocused) const -{ - if (!isFocused) - return; - - if (!m_impl->_window) - { - *isFocused = false; - return; - } - - *isFocused = [NSApp isActive] && [m_impl->_window isKeyWindow]; -} - -unsigned int InfiniFrameWindow::GetScreenDpi() const -{ - return 72; -} - -void InfiniFrameWindow::GetSize(int* width, int* height) const -{ - NSSize size = [m_impl->_window frame].size; - if (width) *width = static_cast(roundf(size.width)); - if (height) *height = static_cast(roundf(size.height)); -} - -void InfiniFrameWindow::GetMaxSize(int* width, int* height) const -{ - NSSize maxSize = [m_impl->_window maxSize]; - if (width) *width = static_cast(roundf(maxSize.width)); - if (height) *height = static_cast(roundf(maxSize.height)); -} - -void InfiniFrameWindow::GetMinSize(int* width, int* height) const -{ - NSSize minSize = [m_impl->_window minSize]; - if (width) *width = static_cast(roundf(minSize.width)); - if (height) *height = static_cast(roundf(minSize.height)); -} - -AutoString InfiniFrameWindow::GetTitle() const -{ - return AllocateStringCopy(m_impl->_windowTitle); -} - -void InfiniFrameWindow::GetTopmost(bool* topmost) const -{ - *topmost = ([m_impl->_window level] & NSFloatingWindowLevel) == NSFloatingWindowLevel; -} - -void InfiniFrameWindow::GetZoom(int* zoom) const -{ - CGFloat rawValue = [m_impl->_webview magnification]; - rawValue = (rawValue * 100.0) + 0.5; - *zoom = static_cast(rawValue); -} - -AutoString InfiniFrameWindow::GetIconFileName() const -{ - 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 -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::SetDevToolsEnabled(bool enabled) -{ - m_impl->_devToolsEnabled = enabled; - m_impl->SetPreference(@"developerExtrasEnabled", enabled ? @YES : @NO); -} - -void InfiniFrameWindow::SetTransparentEnabled(bool enabled) -{ - // Not implemented on macOS -} - -void InfiniFrameWindow::SetContextMenuEnabled(bool enabled) -{ - // Not supported on macOS -} - -void InfiniFrameWindow::SetZoomEnabled(bool enabled) -{ - // Not implemented on macOS -} - -void InfiniFrameWindow::SetIconFile(AutoString filename) -{ - NSString* path = [NSString stringWithUTF8String: filename]; - NSImage* icon = [[NSImage alloc] initWithContentsOfFile: path]; - if (icon != nil) - [[m_impl->_window standardWindowButton: NSWindowDocumentIconButton] setImage: icon]; - - 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]; - if (!screen) screen = [NSScreen mainScreen]; - NSRect screenFrame = [screen frame]; - - NSRect frame = [m_impl->_window frame]; - int height = static_cast(roundf(frame.size.height)); - - auto left = static_cast(x); - auto top = static_cast(screenFrame.origin.y + screenFrame.size.height - (y + height)); - - [m_impl->_window setFrameOrigin: CGPointMake(left, top)]; -} - -void InfiniFrameWindow::SetResizable(bool resizable) -{ - if (resizable) - m_impl->_window.styleMask |= NSWindowStyleMaskResizable; - else - m_impl->_window.styleMask &= ~NSWindowStyleMaskResizable; -} - -void InfiniFrameWindow::SetSize(int width, int height) -{ - width = width > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : width; - height = height > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : height; - - if (width > m_impl->_window.maxSize.width) width = m_impl->_window.maxSize.width; - if (height > m_impl->_window.maxSize.height) height = m_impl->_window.maxSize.height; - if (width < m_impl->_window.minSize.width) width = m_impl->_window.minSize.width; - if (height < m_impl->_window.minSize.height) height = m_impl->_window.minSize.height; - - NSRect frame = [m_impl->_window frame]; - CGFloat oldHeight = frame.size.height; - frame.size = CGSizeMake(static_cast(width), static_cast(height)); - frame.origin.y -= static_cast(height) - oldHeight; - - [m_impl->_window setFrame: frame display: true]; -} - -void InfiniFrameWindow::SetMinSize(int width, int height) -{ - width = width > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : width; - height = height > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : height; - - [m_impl->_window setMinSize: NSMakeSize(width, height)]; -} - -void InfiniFrameWindow::SetMaxSize(int width, int height) -{ - width = width > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : width; - height = height > MAX_WINDOW_DIMENSION ? MAX_WINDOW_DIMENSION : height; - - [m_impl->_window setMaxSize: NSMakeSize(width, height)]; -} - -void InfiniFrameWindow::SetTitle(AutoString title) -{ - m_impl->_windowTitle = title ? title : ""; - [m_impl->_window setTitle: [NSString stringWithUTF8String: title]]; -} - -void InfiniFrameWindow::SetTopmost(bool topmost) -{ - if (topmost) [m_impl->_window setLevel: NSFloatingWindowLevel]; - else [m_impl->_window setLevel: NSNormalWindowLevel]; -} - -void InfiniFrameWindow::SetZoom(int zoom) -{ - CGFloat newZoom = zoom / 100.0; - [m_impl->_webview setMagnification: newZoom]; -} - -void InfiniFrameWindow::SetFocused() -{ - if (!m_impl->_window) return; - - [NSApp activateIgnoringOtherApps: YES]; - [m_impl->_window makeKeyAndOrderFront: m_impl->_window]; - - if (![m_impl->_window isKeyWindow]) - { - [m_impl->_window orderFrontRegardless]; - [m_impl->_window makeKeyWindow]; - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// 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]) { - [NSApp run]; - return; - } - - __block bool windowClosed = false; - id observer = [[NSNotificationCenter defaultCenter] - addObserverForName: NSWindowWillCloseNotification - object: m_impl->_window - queue: nil - usingBlock: ^(NSNotification*) { - windowClosed = true; - }]; - - while (!windowClosed) { - [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode - beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.05]]; - } - - [[NSNotificationCenter defaultCenter] removeObserver: observer]; -} - -void InfiniFrameWindow::CloseWebView() -{ - // Not implemented on macOS -} - -// --------------------------------------------------------------------------------------------------------------------- -// Callbacks -// --------------------------------------------------------------------------------------------------------------------- - -InfiniFrameDialog* InfiniFrameWindow::GetDialog() const -{ - return m_impl->_dialog.get(); -} - -void InfiniFrameWindow::AddCustomSchemeName(const AutoStringConst scheme) -{ - if (scheme) - m_impl->_customSchemeNames.emplace_back(scheme); -} - -void InfiniFrameWindow::GetAllMonitors(GetAllMonitorsCallback callback) const -{ - if (callback) - { - 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); - } - } -} - -void InfiniFrameWindow::SetClosingCallback(const ClosingCallback callback) -{ - m_impl->_closingCallback = callback; -} - -void InfiniFrameWindow::SetClosedCallback(const ClosedCallback callback) -{ - m_impl->_closedCallback = callback; -} - -void InfiniFrameWindow::SetFocusInCallback(const FocusInCallback callback) -{ - m_impl->_focusInCallback = callback; -} - -void InfiniFrameWindow::SetFocusOutCallback(const FocusOutCallback callback) -{ - m_impl->_focusOutCallback = callback; -} - -void InfiniFrameWindow::SetMovedCallback(const MovedCallback callback) -{ - m_impl->_movedCallback = callback; -} - -void InfiniFrameWindow::SetResizedCallback(const ResizedCallback callback) -{ - m_impl->_resizedCallback = callback; -} - -void InfiniFrameWindow::SetMaximizedCallback(const MaximizedCallback callback) -{ - m_impl->_maximizedCallback = callback; -} - -void InfiniFrameWindow::SetRestoredCallback(const RestoredCallback callback) -{ - m_impl->_restoredCallback = callback; -} - -void InfiniFrameWindow::SetMinimizedCallback(const MinimizedCallback callback) -{ - 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) - return m_impl->_closingCallback(); - return false; -} - -void InfiniFrameWindow::InvokeClosed() const noexcept -{ - if (m_impl->_closedCallback) - m_impl->_closedCallback(); -} - -void InfiniFrameWindow::InvokeFocusIn() const noexcept -{ - if (m_impl->_focusInCallback) - m_impl->_focusInCallback(); -} - -void InfiniFrameWindow::InvokeFocusOut() const noexcept -{ - if (m_impl->_focusOutCallback) - m_impl->_focusOutCallback(); -} - -void InfiniFrameWindow::InvokeMove(int x, int y) const noexcept -{ - if (m_impl->_movedCallback) - m_impl->_movedCallback(x, y); -} - -void InfiniFrameWindow::InvokeResize(int width, int height) const noexcept -{ - if (m_impl->_resizedCallback) - m_impl->_resizedCallback(width, height); -} - -void InfiniFrameWindow::InvokeMaximized() const noexcept -{ - if (m_impl->_maximizedCallback) - m_impl->_maximizedCallback(); -} - -void InfiniFrameWindow::InvokeRestored() const noexcept -{ - if (m_impl->_restoredCallback) - m_impl->_restoredCallback(); -} - -void InfiniFrameWindow::InvokeMinimized() const noexcept -{ - if (m_impl->_minimizedCallback) - m_impl->_minimizedCallback(); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Private methods -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::AttachWebView() -{ - auto js = Embedded::InfiniFrameJsUtf8(); - - 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) -{ - if (m_impl->_webview == nil) - AttachWebView(); - - [m_impl->_window makeKeyAndOrderFront: m_impl->_window]; - [m_impl->_window orderFrontRegardless]; -} - -#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WindowDelegate.h b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WindowDelegate.h index d9c42685b..93f3c0729 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WindowDelegate.h +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Mac/WindowDelegate.h @@ -4,7 +4,7 @@ * @file WindowDelegate.h * @brief NSWindow delegate that forwards window lifecycle events to InfiniFrameWindow callbacks */ -#include "Core/InfiniFrame.h" +#include "Public/InfiniFrame.h" /** * @brief Per-window delegate conforming to NSWindowDelegate. diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/UiDispatcher.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/UiDispatcher.Win32.cpp new file mode 100644 index 000000000..81ccfa54c --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/UiDispatcher.Win32.cpp @@ -0,0 +1,75 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "chrono" + +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +void InfiniFrameWindow::WaitForExit() { + auto* impl = m_impl.get(); + ApplyPendingOwnerWindow(impl, L"wait_for_exit"); + + messageLoopRootWindowHandle = impl->_hWnd; + TraceTeardown(L"WaitForExit start instance=%p hwnd=%p", this, impl->_hWnd); + + MSG msg = {}; + while (true) { + const int getMessageResult = GetMessage(&msg, nullptr, 0, 0); + if (getMessageResult == -1) { + TraceTeardown(L"WaitForExit GetMessage failed err=%lu", GetLastError()); + break; + } + if (getMessageResult == 0) + break; + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + messageLoopRootWindowHandle = nullptr; + TraceTeardown(L"WaitForExit end instance=%p hwnd=%p", this, impl->_hWnd); +} + +void InfiniFrameWindow::Invoke(ACTION callback) { + if (!callback) { + return; + } + + auto* impl = m_impl.get(); + if (impl->_hWnd == nullptr || IsWindow(impl->_hWnd) == 0) { + return; + } + + auto* waitInfo = new InvokeWaitInfo(); + if (!PostMessage( + impl->_hWnd, WM_USER_INVOKE, reinterpret_cast(callback), reinterpret_cast(waitInfo) + )) { + delete waitInfo; + return; + } + + std::unique_lock uLock(waitInfo->mutex); + const bool completed = + waitInfo->completionNotifier.wait_for(uLock, std::chrono::seconds(15), [&] { return waitInfo->isCompleted; }); + + if (!completed) { + bool deleteWaitInfo = false; + if (waitInfo->isCompleted) + deleteWaitInfo = true; + else + waitInfo->isAbandoned = true; + + uLock.unlock(); + + if (deleteWaitInfo) + delete waitInfo; + + OutputDebugStringW(L"InfiniFrameWindow::Invoke timed out waiting for UI thread callback.\n"); + return; + } + + uLock.unlock(); + delete waitInfo; +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowCore.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowCore.Win32.cpp new file mode 100644 index 000000000..dc13e6730 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowCore.Win32.cpp @@ -0,0 +1,15 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Utils/Common.h" +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +static_assert(sizeof(wchar_t) == sizeof(char16_t)); + +const wchar_t* CLASS_NAME = L"InfiniFrame"; +std::atomic _hInstance{nullptr}; +thread_local HWND messageLoopRootWindowHandle = nullptr; +wchar_t _webview2RuntimePath[MAX_PATH]; +std::mutex webview2RuntimePathMutex; diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowEncoding.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowEncoding.Win32.cpp new file mode 100644 index 000000000..fa35407f7 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowEncoding.Win32.cpp @@ -0,0 +1,48 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +std::wstring Utf8ToWide(const AutoString source) { + if (source == nullptr) + return {}; + + const auto* utf8 = reinterpret_cast(source); + const size_t utf8Length = strlen(utf8); + if (utf8Length == 0) + return {}; + + if (const auto validation = simdutf::validate_utf8_with_errors(utf8, utf8Length); validation.is_err()) + return {}; + + std::u16string utf16(simdutf::utf16_length_from_utf8(utf8, utf8Length), u'\0'); + const size_t written = + simdutf::convert_valid_utf8_to_utf16(utf8, utf8Length, reinterpret_cast(utf16.data())); + utf16.resize(written); + + return {reinterpret_cast(utf16.data()), utf16.size()}; +} + +std::string WideToUtf8(const AutoString 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; +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowEvents.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowEvents.Win32.cpp new file mode 100644 index 000000000..679ded74d --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowEvents.Win32.cpp @@ -0,0 +1,142 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include + +#include "Platform/Windows/Window.Win32.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +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::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::GetAllMonitors(GetAllMonitorsCallback callback) const { + if (callback) { + EnumDisplayMonitors( + nullptr, nullptr, reinterpret_cast(MonitorEnum), reinterpret_cast(callback) + ); + } +} + +InfiniFrameDialog* InfiniFrameWindow::GetDialog() const { + return m_impl->_dialog.get(); +} + +void InfiniFrameWindow::AddCustomSchemeName(const AutoStringConst scheme) { + if (scheme) + m_impl->_customSchemeNames.emplace_back(ToUTF16String(const_cast(scheme))); +} + +void InfiniFrameWindow::SetClosingCallback(const ClosingCallback callback) { + m_impl->_closingCallback = callback; +} + +void InfiniFrameWindow::SetClosedCallback(const ClosedCallback callback) { + m_impl->_closedCallback = callback; +} + +void InfiniFrameWindow::SetFocusInCallback(const FocusInCallback callback) { + m_impl->_focusInCallback = callback; +} + +void InfiniFrameWindow::SetFocusOutCallback(const FocusOutCallback callback) { + m_impl->_focusOutCallback = callback; +} + +void InfiniFrameWindow::SetMovedCallback(const MovedCallback callback) { + m_impl->_movedCallback = callback; +} + +void InfiniFrameWindow::SetResizedCallback(const ResizedCallback callback) { + m_impl->_resizedCallback = callback; +} + +void InfiniFrameWindow::SetMaximizedCallback(const MaximizedCallback callback) { + m_impl->_maximizedCallback = callback; +} + +void InfiniFrameWindow::SetRestoredCallback(const RestoredCallback callback) { + m_impl->_restoredCallback = callback; +} + +void InfiniFrameWindow::SetMinimizedCallback(const MinimizedCallback callback) { + m_impl->_minimizedCallback = callback; +} + +bool InfiniFrameWindow::InvokeClose() const noexcept { + if (m_impl->_closingCallback) + return m_impl->_closingCallback(); + return false; +} + +void InfiniFrameWindow::InvokeClosed() const noexcept { + if (!m_impl->_closedCallback) + return; + m_impl->_closedCallback(); +} + +void InfiniFrameWindow::InvokeFocusIn() const noexcept { + if (m_impl->_focusInCallback) + m_impl->_focusInCallback(); +} + +void InfiniFrameWindow::InvokeFocusOut() const noexcept { + if (m_impl->_focusOutCallback) + m_impl->_focusOutCallback(); +} + +void InfiniFrameWindow::InvokeMove(int x, int y) const noexcept { + if (m_impl->_movedCallback) + m_impl->_movedCallback(x, y); +} + +void InfiniFrameWindow::InvokeResize(int width, int height) const noexcept { + if (m_impl->_resizedCallback) + m_impl->_resizedCallback(width, height); +} + +void InfiniFrameWindow::InvokeMaximized() const noexcept { + if (m_impl->_maximizedCallback) + m_impl->_maximizedCallback(); +} + +void InfiniFrameWindow::InvokeRestored() const noexcept { + if (m_impl->_restoredCallback) + m_impl->_restoredCallback(); +} + +void InfiniFrameWindow::InvokeMinimized() const noexcept { + if (m_impl->_minimizedCallback) + m_impl->_minimizedCallback(); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowLifecycle.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowLifecycle.Win32.cpp new file mode 100644 index 000000000..327e0efdc --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowLifecycle.Win32.cpp @@ -0,0 +1,270 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include + +#include "Platform/Windows/DarkMode.h" +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +using namespace WinToastLib; + +LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + +namespace { + 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 + +HBRUSH GetDarkBrush() { + return BrushManager::instance().dark(); +} + +HBRUSH GetLightBrush() { + return BrushManager::instance().light(); +} + +void InfiniFrameWindow::Register(const HINSTANCE hInstance) { + InitDarkModeSupport(); + + _hInstance.store(hInstance, std::memory_order_release); + + WNDCLASSEX wcx; + wcx.cbSize = sizeof(WNDCLASSEX); + wcx.style = CS_HREDRAW | CS_VREDRAW; + wcx.lpfnWndProc = WindowProc; + wcx.cbClsExtra = 0; + wcx.cbWndExtra = 0; + wcx.hInstance = hInstance; + wcx.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + wcx.hCursor = LoadCursor(nullptr, IDC_ARROW); + wcx.hbrBackground = IsDarkModeEnabled() ? GetDarkBrush() : GetLightBrush(); + wcx.lpszMenuName = nullptr; + wcx.lpszClassName = CLASS_NAME; + wcx.hIconSm = LoadIcon(hInstance, IDI_APPLICATION); + + RegisterClassEx(&wcx); + + SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); +} + +// Initializes native window lifecycle state from host-provided startup parameters. +// Flow: +// 1) Allocate implementation storage. +// 2) Validate ABI compatibility of InfiniFrameInitParams via StructSize. +// 3) Configure window identity/notifications and startup payload values. +// 4) Continue with remaining platform/window initialization in this constructor. +InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { + // Backing implementation object must exist before any field assignment. + m_impl = std::make_unique(); + + // Fail fast if caller and native side disagree on struct layout/version. + if (initParams->StructSize != sizeof(InfiniFrameInitParams)) { + auto msg = std::format( + L"Initial parameters passed are {} bytes, but expected {} bytes.", initParams->StructSize, + sizeof(InfiniFrameInitParams) + ); + MessageBox(nullptr, msg.c_str(), L"Native Initialization Failed", MB_OK); + exit(0); + } + + // Initialize window title and optional toast notification identity. + 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()); + } + } + + // Capture startup URL (if provided) for initial navigation/bootstrap. + if (initParams->StartUrl != nullptr) + m_impl->_startUrl = ToUTF16String(initParams->StartUrl); + + // Capture startup string payload (if provided) for host-defined boot data. + if (initParams->StartString != nullptr) + m_impl->_startString = ToUTF16String(initParams->StartString); + + if (initParams->TemporaryFilesPath != nullptr) + m_impl->_temporaryFilesPath = ToUTF16String(initParams->TemporaryFilesPath); + + if (initParams->UserAgent != nullptr) + m_impl->_userAgent = ToUTF16String(initParams->UserAgent); + + if (initParams->BrowserControlInitParameters != nullptr) + m_impl->_browserControlInitParameters = ToUTF16String(initParams->BrowserControlInitParameters); + + if (initParams->NotificationRegistrationId != nullptr) + m_impl->_notificationRegistrationId = ToUTF16String(initParams->NotificationRegistrationId); + + m_impl->_transparentEnabled = initParams->Transparent; + m_impl->_contextMenuEnabled = initParams->ContextMenuEnabled; + m_impl->_zoomEnabled = initParams->ZoomEnabled; + m_impl->_devToolsEnabled = initParams->DevToolsEnabled; + m_impl->_grantBrowserPermissions = initParams->GrantBrowserPermissions; + m_impl->_mediaAutoplayEnabled = initParams->MediaAutoplayEnabled; + m_impl->_fileSystemAccessEnabled = initParams->FileSystemAccessEnabled; + m_impl->_webSecurityEnabled = initParams->WebSecurityEnabled; + m_impl->_javascriptClipboardAccessEnabled = initParams->JavascriptClipboardAccessEnabled; + m_impl->_mediaStreamEnabled = initParams->MediaStreamEnabled; + m_impl->_smoothScrollingEnabled = initParams->SmoothScrollingEnabled; + m_impl->_ignoreCertificateErrorsEnabled = initParams->IgnoreCertificateErrorsEnabled; + m_impl->_notificationsEnabled = initParams->NotificationsEnabled; + + m_impl->_zoom = initParams->Zoom; + m_impl->_minWidth = initParams->MinWidth; + m_impl->_minHeight = initParams->MinHeight; + m_impl->_maxWidth = initParams->MaxWidth; + m_impl->_maxHeight = initParams->MaxHeight; + + m_impl->_webMessageReceivedCallback = initParams->WebMessageReceivedHandler; + m_impl->_resizedCallback = initParams->ResizedHandler; + m_impl->_maximizedCallback = initParams->MaximizedHandler; + m_impl->_restoredCallback = initParams->RestoredHandler; + m_impl->_minimizedCallback = initParams->MinimizedHandler; + m_impl->_movedCallback = initParams->MovedHandler; + m_impl->_closingCallback = initParams->ClosingHandler; + m_impl->_closedCallback = initParams->ClosedHandler; + m_impl->_focusInCallback = initParams->FocusInHandler; + m_impl->_focusOutCallback = initParams->FocusOutHandler; + m_impl->_customSchemeCallback = initParams->CustomSchemeHandler; + + for (int i = 0; i < 16; ++i) { + if (initParams->CustomSchemeNames[i] != nullptr) + m_impl->_customSchemeNames.emplace_back(ToUTF16String(initParams->CustomSchemeNames[i])); + } + + m_impl->_parent = initParams->ParentInstance; + + int normalizedWidth = initParams->Width; + int normalizedHeight = initParams->Height; + int normalizedLeft = initParams->Left; + int normalizedTop = initParams->Top; + bool centerOnInitialize = initParams->CenterOnInitialize; + + if (initParams->UseOsDefaultSize) { + normalizedWidth = CW_USEDEFAULT; + normalizedHeight = CW_USEDEFAULT; + } else { + if (normalizedWidth < 0) + normalizedWidth = CW_USEDEFAULT; + if (normalizedHeight < 0) + normalizedHeight = CW_USEDEFAULT; + } + + if (initParams->UseOsDefaultLocation) { + normalizedLeft = CW_USEDEFAULT; + normalizedTop = CW_USEDEFAULT; + } + + if (initParams->FullScreen) { + normalizedLeft = 0; + normalizedTop = 0; + normalizedWidth = GetSystemMetrics(SM_CXSCREEN); + normalizedHeight = GetSystemMetrics(SM_CYSCREEN); + } + + if (initParams->Chromeless) { + if (normalizedLeft == CW_USEDEFAULT && normalizedTop == CW_USEDEFAULT) + centerOnInitialize = true; + if (normalizedLeft == CW_USEDEFAULT) + normalizedLeft = 0; + if (normalizedTop == CW_USEDEFAULT) + normalizedTop = 0; + if (normalizedHeight == CW_USEDEFAULT) + normalizedHeight = 600; + if (normalizedWidth == CW_USEDEFAULT) + normalizedWidth = 800; + } + + if (normalizedHeight > initParams->MaxHeight) + normalizedHeight = initParams->MaxHeight; + if (normalizedHeight < initParams->MinHeight && initParams->MinHeight > 0) + normalizedHeight = initParams->MinHeight; + if (normalizedWidth > initParams->MaxWidth) + normalizedWidth = initParams->MaxWidth; + if (normalizedWidth < initParams->MinWidth && initParams->MinWidth > 0) + normalizedWidth = initParams->MinWidth; + + const HWND parentWindowHandle = ResolveParentWindowHandle(m_impl->_parent); + m_impl->_pendingOwnerHwnd = parentWindowHandle; + + const HINSTANCE windowInstance = _hInstance.load(std::memory_order_acquire); + m_impl->_hWnd = CreateWindowEx( + initParams->Transparent ? WS_EX_LAYERED : 0, CLASS_NAME, m_impl->_windowTitle.c_str(), + initParams->Chromeless || initParams->FullScreen ? WS_POPUP : WS_OVERLAPPEDWINDOW, normalizedLeft, + normalizedTop, normalizedWidth, normalizedHeight, nullptr, nullptr, windowInstance, this + ); + + ApplyPendingOwnerWindow(m_impl.get(), L"ctor"); + + if (initParams->WindowIconFile != nullptr) { + SetIconFile(initParams->WindowIconFile); + } + + if (centerOnInitialize) + Center(); + + if (initParams->Minimized) + SetMinimized(true); + + if (initParams->Maximized) + SetMaximized(true); + + SetResizable(initParams->Resizable); + + 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->_dialog = std::make_unique(this); + + bool isAlreadyShown = initParams->Minimized || initParams->Maximized; + Show(isAlreadyShown); +} + +InfiniFrameWindow::~InfiniFrameWindow() {} + +HWND InfiniFrameWindow::getHwnd() { + return m_impl->_hWnd; +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowOwnership.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowOwnership.Win32.cpp new file mode 100644 index 000000000..39f9941e1 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowOwnership.Win32.cpp @@ -0,0 +1,21 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +InfiniFrameWindow* LookupWindowInstance(const HWND hwnd) { + return reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); +} + +HWND ResolveParentWindowHandle(InfiniFrameWindow* parent) { + if (parent == nullptr) + return nullptr; + + HWND parentHwnd = parent->getHwnd(); + if (parentHwnd == nullptr || !IsWindow(parentHwnd)) + return nullptr; + + return parentHwnd; +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowProc.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowProc.Win32.cpp new file mode 100644 index 000000000..c360fdc53 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowProc.Win32.cpp @@ -0,0 +1,165 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Platform/Windows/DarkMode.h" +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +// Central Win32 message dispatcher for an InfiniFrame top-level window. +// This procedure coordinates native lifecycle events with managed/window context state: +// - stores and retrieves the InfiniFrameWindow instance +// - reacts to theme and color-scheme changes (dark/light mode) +// - applies per-monitor DPI resize recommendations +// - forwards focus and close events to the owning instance +// - paints the window background according to current theme +LRESULT CALLBACK WindowProc(const HWND hwnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) { + switch (uMsg) { + case WM_NCCREATE: { + // Capture the instance pointer at non-client creation so later messages can + // resolve window state via GWLP_USERDATA. + const auto* createParams = reinterpret_cast(lParam); + auto* instance = reinterpret_cast(createParams->lpCreateParams); + SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(instance)); + return TRUE; + } + case WM_CREATE: { + // Initialize dark mode support once the window is created. + EnableDarkMode(hwnd, true); + if (IsDarkModeEnabled()) + RefreshNonClientArea(hwnd); + break; + } + case WM_DPICHANGED: { + // Use the system-provided suggested rectangle to keep the window properly sized + // and positioned when moving between monitors with different DPI. + 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: { + // Forward color-scheme changes to the same theme-refresh path used by WM_THEMECHANGED. + if (IsColorSchemeChange(lParam)) + SendMessageW(hwnd, WM_THEMECHANGED, 0, 0); + + break; + } + case WM_THEMECHANGED: { + // Reapply dark mode and redraw client/non-client regions after a theme transition. + EnableDarkMode(hwnd, IsDarkModeEnabled()); + RefreshNonClientArea(hwnd); + InvalidateRect(hwnd, nullptr, TRUE); + break; + } + case WM_PAINT: { + // Paint only the invalidated region with the active theme background brush. + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + + if (IsDarkModeEnabled()) { + FillRect(hdc, &ps.rcPaint, GetDarkBrush()); + } else { + FillRect(hdc, &ps.rcPaint, GetLightBrush()); + } + + EndPaint(hwnd, &ps); + break; + } + case WM_ACTIVATE: { + // Keep WebView/focus state synchronized with native activation transitions. + InfiniFrameWindow* instance = LookupWindowInstance(hwnd); + if (instance) { + if (LOWORD(wParam) == WA_INACTIVE) { + instance->InvokeFocusOut(); + } else { + instance->FocusWebView2(); + instance->InvokeFocusIn(); + + return 0; + } + } + break; + } + case WM_CLOSE: { + // Give the instance a chance to cancel close. If close proceeds, clear owner + // relationship before destruction to avoid shutdown-order and ownership edge cases. + InfiniFrameWindow* instance = LookupWindowInstance(hwnd); + if (instance) { + TraceTeardown(L"WM_CLOSE hwnd=%p instance=%p", hwnd, instance); + bool doNotClose = instance->InvokeClose(); + + if (!doNotClose) { + SetLastError(0); + const LONG_PTR previousOwner = SetWindowLongPtr(hwnd, GWLP_HWNDPARENT, 0); + const DWORD ownerDetachError = GetLastError(); + if (previousOwner != 0 || ownerDetachError == 0) { + TraceTeardown( + L"WM_CLOSE detached owner hwnd=%p prevOwner=%p err=%lu", hwnd, + reinterpret_cast(previousOwner), ownerDetachError + ); + } + + DestroyWindow(hwnd); + } + } + + return 0; + } + case WM_DESTROY: { + InfiniFrameWindow* instance = LookupWindowInstance(hwnd); + if (instance) { + instance->m_impl->_isClosingOrClosed.store(true, std::memory_order_release); + TraceTeardown(L"WM_DESTROY begin hwnd=%p instance=%p", hwnd, instance); + instance->CloseWebView(); + instance->InvokeClosed(); + TraceTeardown(L"WM_DESTROY end hwnd=%p instance=%p", hwnd, instance); + } + if (hwnd == messageLoopRootWindowHandle) + PostQuitMessage(0); + + return 0; + } + case WM_NCDESTROY: { + InfiniFrameWindow* instance = LookupWindowInstance(hwnd); + if (instance) { + instance->m_impl->_isClosingOrClosed.store(true, std::memory_order_release); + instance->m_impl->_hWnd = nullptr; + } + TraceTeardown(L"WM_NCDESTROY hwnd=%p instance=%p", hwnd, instance); + SetWindowLongPtr(hwnd, GWLP_USERDATA, 0); + break; + } + case WM_USER_INVOKE: { + auto callback = reinterpret_cast(wParam); + auto* waitInfo = reinterpret_cast(lParam); + + if (waitInfo == nullptr) { + if (callback) + callback(); + return 0; + } + + bool deleteWaitInfo = false; + { + std::lock_guard guard(waitInfo->mutex); + if (!waitInfo->isAbandoned && callback) + callback(); + waitInfo->isCompleted = true; + deleteWaitInfo = waitInfo->isAbandoned; + } + + waitInfo->completionNotifier.notify_one(); + + if (deleteWaitInfo) + delete waitInfo; + return 0; + } + } + + return DefWindowProc(hwnd, uMsg, wParam, lParam); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowState.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowState.Win32.cpp new file mode 100644 index 000000000..43c58dc0d --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowState.Win32.cpp @@ -0,0 +1,459 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include + +#include "Utils/Common.h" +#include "Platform/Windows/Window.Win32.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +void InfiniFrameWindow::Center() { + int screenDpi = GetDpiForWindow(m_impl->_hWnd); + int screenHeight = GetSystemMetricsForDpi(SM_CYSCREEN, screenDpi); + int screenWidth = GetSystemMetricsForDpi(SM_CXSCREEN, screenDpi); + + RECT windowRect = {}; + GetWindowRect(m_impl->_hWnd, &windowRect); + int windowHeight = windowRect.bottom - windowRect.top; + int windowWidth = windowRect.right - windowRect.left; + + int left = (screenWidth / 2) - (windowWidth / 2); + int top = (screenHeight / 2) - (windowHeight / 2); + + SetPosition(left, top); +} + +void InfiniFrameWindow::Close() { + PostMessage(m_impl->_hWnd, WM_CLOSE, 0, 0); +} + +void InfiniFrameWindow::GetTransparentEnabled(bool* enabled) const { + if (!m_impl->_webviewController) { + *enabled = m_impl->_transparentEnabled; + return; + } + wil::com_ptr controller2; + if (FAILED(m_impl->_webviewController->QueryInterface(&controller2)) || !controller2) { + *enabled = m_impl->_transparentEnabled; + return; + } + COREWEBVIEW2_COLOR backgroundColor; + controller2->get_DefaultBackgroundColor(&backgroundColor); + *enabled = backgroundColor.A == 0; +} + +void InfiniFrameWindow::GetContextMenuEnabled(bool* enabled) const { + if (!m_impl->_webviewWindow) { + *enabled = m_impl->_contextMenuEnabled; + return; + } + wil::com_ptr settings; + if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { + BOOL boolValue = FALSE; + settings->get_AreDefaultContextMenusEnabled(&boolValue); + *enabled = (boolValue != FALSE); + } +} + +void InfiniFrameWindow::GetZoomEnabled(bool* enabled) const { + if (!m_impl->_webviewWindow) { + *enabled = m_impl->_zoomEnabled; + return; + } + wil::com_ptr settings; + if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { + BOOL boolValue = FALSE; + settings->get_IsZoomControlEnabled(&boolValue); + *enabled = (boolValue != FALSE); + } +} + +void InfiniFrameWindow::GetDevToolsEnabled(bool* enabled) const { + if (!m_impl->_webviewWindow) { + *enabled = m_impl->_devToolsEnabled; + return; + } + wil::com_ptr settings; + if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { + BOOL boolValue = FALSE; + settings->get_AreDevToolsEnabled(&boolValue); + *enabled = (boolValue != FALSE); + } +} + +void InfiniFrameWindow::GetFullScreen(bool* fullScreen) const { + LONG lStyles = GetWindowLong(m_impl->_hWnd, GWL_STYLE); + *fullScreen = (lStyles & WS_POPUP) != 0; +} + +void InfiniFrameWindow::GetGrantBrowserPermissions(bool* grant) const { + *grant = m_impl->_grantBrowserPermissions; +} + +AutoString InfiniFrameWindow::GetUserAgent() const { + return AllocateStringCopy(m_impl->_userAgent); +} + +void InfiniFrameWindow::GetMediaAutoplayEnabled(bool* enabled) const { + *enabled = m_impl->_mediaAutoplayEnabled; +} + +void InfiniFrameWindow::GetFileSystemAccessEnabled(bool* enabled) const { + *enabled = m_impl->_fileSystemAccessEnabled; +} + +void InfiniFrameWindow::GetWebSecurityEnabled(bool* enabled) const { + *enabled = m_impl->_webSecurityEnabled; +} + +void InfiniFrameWindow::GetJavascriptClipboardAccessEnabled(bool* enabled) const { + *enabled = m_impl->_javascriptClipboardAccessEnabled; +} + +void InfiniFrameWindow::GetMediaStreamEnabled(bool* enabled) const { + *enabled = m_impl->_mediaStreamEnabled; +} + +void InfiniFrameWindow::GetSmoothScrollingEnabled(bool* enabled) const { + *enabled = m_impl->_smoothScrollingEnabled; +} + +void InfiniFrameWindow::GetIgnoreCertificateErrorsEnabled(bool* enabled) const { + *enabled = m_impl->_ignoreCertificateErrorsEnabled; +} + +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); +} + +void InfiniFrameWindow::GetMaximized(bool* isMaximized) const { + LONG lStyles = GetWindowLong(m_impl->_hWnd, GWL_STYLE); + *isMaximized = (lStyles & WS_MAXIMIZE) != 0; +} + +void InfiniFrameWindow::GetMinimized(bool* isMinimized) const { + LONG lStyles = GetWindowLong(m_impl->_hWnd, GWL_STYLE); + *isMinimized = (lStyles & WS_MINIMIZE) != 0; +} + +void InfiniFrameWindow::GetPosition(int* x, int* y) const { + RECT rect = {}; + GetWindowRect(m_impl->_hWnd, &rect); + if (x) + *x = rect.left; + if (y) + *y = rect.top; +} + +void InfiniFrameWindow::GetResizable(bool* resizable) const { + LONG lStyles = GetWindowLong(m_impl->_hWnd, GWL_STYLE); + *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); + if (width) + *width = rect.right - rect.left; + if (height) + *height = rect.bottom - rect.top; +} + +void InfiniFrameWindow::GetMaxSize(int* width, int* height) const { + if (width) + *width = m_impl->_maxWidth; + if (height) + *height = m_impl->_maxHeight; +} + +void InfiniFrameWindow::GetMinSize(int* width, int* height) const { + if (width) + *width = m_impl->_minWidth; + if (height) + *height = m_impl->_minHeight; +} + +AutoString InfiniFrameWindow::GetTitle() const { + return AllocateStringCopy(m_impl->_windowTitle); +} + +void InfiniFrameWindow::GetTopmost(bool* topmost) const { + *topmost = m_impl->_topmost; +} + +void InfiniFrameWindow::GetZoom(int* zoom) const { + if (zoom == nullptr) + return; + if (m_impl->_webviewController == nullptr) { + *zoom = m_impl->_zoom; + return; + } + + double rawValue = 0; + if (FAILED(m_impl->_webviewController->get_ZoomFactor(&rawValue))) { + *zoom = m_impl->_zoom; + return; + } + + rawValue = (rawValue * 100.0) + 0.5; + *zoom = static_cast(rawValue); +} + +void InfiniFrameWindow::NavigateToString(AutoString content) { + std::wstring wideContent = ToUTF16String(content); + m_impl->_webviewWindow->NavigateToString(wideContent.c_str()); +} + +void InfiniFrameWindow::NavigateToUrl(AutoString url) { + std::wstring wideUrl = ToUTF16String(url); + m_impl->_webviewWindow->Navigate(wideUrl.c_str()); +} + +void InfiniFrameWindow::Restore() { + ShowWindow(m_impl->_hWnd, SW_RESTORE); +} + +void InfiniFrameWindow::SendWebMessage(AutoString message) { + if (!m_impl->_webviewWindow || !m_impl->_webviewController || !m_impl->_hWnd || !IsWindow(m_impl->_hWnd)) + return; + + std::wstring wideMessage = ToUTF16String(message); + m_impl->_webviewWindow->PostWebMessageAsString(wideMessage.c_str()); +} + +void InfiniFrameWindow::SetTransparentEnabled(const bool enabled) { + m_impl->_transparentEnabled = enabled; + if (!m_impl->_webviewController || !m_impl->_webviewWindow) + return; + wil::com_ptr controller2; + if (FAILED(m_impl->_webviewController->QueryInterface(&controller2)) || !controller2) + return; + COREWEBVIEW2_COLOR backgroundColor; + controller2->get_DefaultBackgroundColor(&backgroundColor); + backgroundColor.A = enabled ? 0 : 255; + controller2->put_DefaultBackgroundColor(backgroundColor); + m_impl->_webviewWindow->Reload(); +} + +void InfiniFrameWindow::SetContextMenuEnabled(const bool enabled) { + m_impl->_contextMenuEnabled = enabled; + if (!m_impl->_webviewWindow) + return; + wil::com_ptr settings; + if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { + settings->put_AreDefaultContextMenusEnabled(enabled); + m_impl->_webviewWindow->Reload(); + } +} + +void InfiniFrameWindow::SetZoomEnabled(const bool enabled) { + m_impl->_zoomEnabled = enabled; + if (!m_impl->_webviewWindow) + return; + wil::com_ptr settings; + if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { + settings->put_IsZoomControlEnabled(enabled); + m_impl->_webviewWindow->Reload(); + } +} + +void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { + m_impl->_devToolsEnabled = enabled; + if (!m_impl->_webviewWindow) + return; + wil::com_ptr settings; + if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { + settings->put_AreDevToolsEnabled(enabled); + m_impl->_webviewWindow->Reload(); + } +} + +void InfiniFrameWindow::SetFullScreen(const bool fullScreen) { + LONG_PTR style = GetWindowLongPtr(m_impl->_hWnd, GWL_STYLE); + if (fullScreen) { + GetWindowRect(m_impl->_hWnd, &m_impl->_savedRect); + m_impl->_hasSavedRect = true; + + style |= WS_POPUP; + style &= (~WS_OVERLAPPEDWINDOW); + SetWindowLongPtr(m_impl->_hWnd, GWL_STYLE, style); + + HMONITOR monitor = MonitorFromWindow(m_impl->_hWnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitorInfo = {sizeof(monitorInfo)}; + + if (GetMonitorInfoW(monitor, &monitorInfo)) { + RECT rc = monitorInfo.rcMonitor; + SetWindowPos( + m_impl->_hWnd, HWND_TOP, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, + SWP_FRAMECHANGED | SWP_NOOWNERZORDER + ); + } else { + SetWindowPos( + m_impl->_hWnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), + SWP_FRAMECHANGED | SWP_NOOWNERZORDER + ); + } + } else { + style |= WS_OVERLAPPEDWINDOW; + style &= (~WS_POPUP); + SetWindowLongPtr(m_impl->_hWnd, GWL_STYLE, style); + + if (m_impl->_hasSavedRect) { + RECT& r = m_impl->_savedRect; + SetWindowPos( + m_impl->_hWnd, HWND_TOP, r.left, r.top, r.right - r.left, r.bottom - r.top, + SWP_FRAMECHANGED | SWP_NOOWNERZORDER + ); + m_impl->_hasSavedRect = false; + } + } +} + +void InfiniFrameWindow::SetIconFile(const AutoString filename) { + std::wstring wideFilename = ToUTF16String(filename); + m_impl->_iconFileName = wideFilename; + if (wideFilename.empty()) + return; + + HICON iconSmall = static_cast( + LoadImageW(nullptr, wideFilename.c_str(), IMAGE_ICON, 16, 16, LR_LOADFROMFILE | LR_LOADTRANSPARENT | LR_SHARED) + ); + HICON iconBig = static_cast( + LoadImageW(nullptr, wideFilename.c_str(), IMAGE_ICON, 32, 32, LR_LOADFROMFILE | LR_LOADTRANSPARENT | LR_SHARED) + ); + + if (iconSmall && iconBig) { + SendMessageW(m_impl->_hWnd, WM_SETICON, ICON_SMALL, reinterpret_cast(iconSmall)); + SendMessageW(m_impl->_hWnd, WM_SETICON, ICON_BIG, reinterpret_cast(iconBig)); + } +} + +void InfiniFrameWindow::SetMinimized(const bool minimized) { + if (minimized) + ShowWindow(m_impl->_hWnd, SW_MINIMIZE); + else + ShowWindow(m_impl->_hWnd, SW_NORMAL); +} + +void InfiniFrameWindow::SetMinSize(const int width, const int height) { + m_impl->_minWidth = width; + m_impl->_minHeight = height; + + int currWidth, currHeight; + GetSize(&currWidth, &currHeight); + if (currWidth < m_impl->_minWidth) + SetSize(m_impl->_minWidth, currHeight); + if (currHeight < m_impl->_minHeight) + SetSize(currWidth, m_impl->_minHeight); +} + +void InfiniFrameWindow::SetMaximized(const bool maximized) { + if (maximized) + ShowWindow(m_impl->_hWnd, SW_MAXIMIZE); + else + ShowWindow(m_impl->_hWnd, SW_NORMAL); +} + +void InfiniFrameWindow::SetMaxSize(const int width, const int height) { + m_impl->_maxWidth = width; + m_impl->_maxHeight = height; + + int currWidth, currHeight; + GetSize(&currWidth, &currHeight); + if (currWidth > m_impl->_maxWidth) + SetSize(m_impl->_maxWidth, currHeight); + if (currHeight > m_impl->_maxHeight) + SetSize(currWidth, m_impl->_maxHeight); +} + +void InfiniFrameWindow::SetPosition(const int x, const int y) { + SetWindowPos(m_impl->_hWnd, HWND_TOP, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER); +} + +void InfiniFrameWindow::SetResizable(const bool resizable) { + LONG_PTR style = GetWindowLongPtr(m_impl->_hWnd, GWL_STYLE); + if (resizable) + style |= WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; + else + style &= (~WS_THICKFRAME) & (~WS_MINIMIZEBOX) & (~WS_MAXIMIZEBOX); + SetWindowLongPtr(m_impl->_hWnd, GWL_STYLE, style); +} + +void InfiniFrameWindow::SetSize(const int width, const int height) { + SetWindowPos(m_impl->_hWnd, HWND_TOP, 0, 0, width, height, SWP_NOMOVE | SWP_NOZORDER); +} + +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) { + WinToastLib::WinToast::instance()->setAppName(wideTitle.c_str()); + if (m_impl->_notificationRegistrationId.empty()) + WinToastLib::WinToast::instance()->setAppUserModelId(wideTitle.c_str()); + } +} + +void InfiniFrameWindow::SetTopmost(const bool topmost) { + m_impl->_topmost = topmost; + LONG_PTR style = GetWindowLongPtr(m_impl->_hWnd, GWL_EXSTYLE); + if (topmost) + style |= WS_EX_TOPMOST; + else + style &= (~WS_EX_TOPMOST); + SetWindowLongPtr(m_impl->_hWnd, GWL_EXSTYLE, style); + SetWindowPos(m_impl->_hWnd, topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); +} + +void InfiniFrameWindow::SetZoom(const int zoom) { + if (zoom < 25 || zoom > 500) + return; + + m_impl->_zoom = zoom; + if (m_impl->_webviewController == nullptr) + return; + + const double newZoom = zoom / 100.0; + m_impl->_webviewController->put_ZoomFactor(newZoom); +} + +void InfiniFrameWindow::SetFocused() { + if (!m_impl->_hWnd) + return; + + if (IsIconic(m_impl->_hWnd)) + ShowWindow(m_impl->_hWnd, SW_RESTORE); + + AllowSetForegroundWindow(ASFW_ANY); + + HWND hwndForeground = GetForegroundWindow(); + const DWORD fgThread = hwndForeground ? GetWindowThreadProcessId(hwndForeground, nullptr) : 0; + const DWORD thisThread = GetCurrentThreadId(); + + if (fgThread && fgThread != thisThread) + AttachThreadInput(fgThread, thisThread, TRUE); + + ShowWindow(m_impl->_hWnd, SW_SHOW); + SetForegroundWindow(m_impl->_hWnd); + BringWindowToTop(m_impl->_hWnd); + SetActiveWindow(m_impl->_hWnd); + SetFocus(m_impl->_hWnd); + + if (fgThread && fgThread != thisThread) + AttachThreadInput(fgThread, thisThread, FALSE); + + FocusWebView2(); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowStorage.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowStorage.Win32.cpp new file mode 100644 index 000000000..d2777d8b3 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowStorage.Win32.cpp @@ -0,0 +1,38 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +bool EnsureDirectoryWritable(const std::wstring& directoryPath) { + if (directoryPath.empty()) + return false; + + std::error_code createError; + std::filesystem::create_directories(directoryPath, createError); + if (createError) + return false; + + const std::wstring probePath = std::format( + L"{}\\{}.tmp", directoryPath, + std::format( + L".infiniframe-wv2-write-check-{}-{}-{}", GetCurrentProcessId(), GetCurrentThreadId(), GetTickCount64() + ) + ); + + HANDLE probeHandle = CreateFileW( + probePath.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, nullptr + ); + + if (probeHandle == INVALID_HANDLE_VALUE) + return false; + + CloseHandle(probeHandle); + DeleteFileW(probePath.c_str()); + return true; +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowTracing.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowTracing.Win32.cpp new file mode 100644 index 000000000..ba6991405 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Core/WindowTracing.Win32.cpp @@ -0,0 +1,52 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include +#include +#include + +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- + +/** + * Determines whether teardown trace logging is enabled in the system. + * + * This function typically checks a configuration setting or a runtime + * flag that specifies if detailed logging or tracing should be + * performed during the teardown phase of a system or application + * component. + * + * @return true if teardown trace logging is enabled, false otherwise. + */ +bool IsTeardownTraceEnabled() { + static const bool enabled = [] { + wchar_t value[32] = {}; + const DWORD len = GetEnvironmentVariableW(L"INFINIFRAME_TRACE_TEARDOWN", value, _countof(value)); + if (len == 0 || len >= _countof(value)) + return false; + + return _wcsicmp(value, L"1") == 0 || _wcsicmp(value, L"true") == 0 || _wcsicmp(value, L"yes") == 0 || + _wcsicmp(value, L"on") == 0; + }(); + + return enabled; +} + +void TraceTeardown(const wchar_t* format, ...) { + if (!IsTeardownTraceEnabled()) + return; + + wchar_t message[1024] = {}; + va_list args; + va_start(args, format); + _vsnwprintf_s(message, _countof(message), _TRUNCATE, format, args); + va_end(args); + + const std::wstring line = std::format(L"[InfiniFrame][teardown][tid={}] {}\n", GetCurrentThreadId(), message); + OutputDebugStringW(line.c_str()); + std::fwprintf(stderr, L"%ls", line.c_str()); + std::fflush(stderr); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/DarkMode.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/DarkMode.cpp index 0dc18ec50..a39844573 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/DarkMode.cpp +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/DarkMode.cpp @@ -1,11 +1,15 @@ -#include "DarkMode.h" - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #include +#include "Platform/Windows/DarkMode.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- using RtlGetNtVersionNumbers_f = void(WINAPI*)(LPDWORD, LPDWORD, LPDWORD); -using SetWindowCompositionAttribute_f = -HRESULT(WINAPI*)(HWND, WINDOWCOMPOSITIONATTRIBDATA*); +using SetWindowCompositionAttribute_f = HRESULT(WINAPI*)(HWND, WINDOWCOMPOSITIONATTRIBDATA*); using ShouldAppsUseDarkMode_f = BOOLEAN(WINAPI*)(); @@ -15,197 +19,169 @@ using RefreshImmersiveColorPolicyState_f = void(WINAPI*)(); using IsDarkModeAllowedForWindow_f = BOOLEAN(WINAPI*)(HWND); -using GetIsImmersiveColorUsingHighContrast_f = -BOOLEAN(WINAPI*)(IMMERSIVE_HC_CACHE_MODE); +using GetIsImmersiveColorUsingHighContrast_f = BOOLEAN(WINAPI*)(IMMERSIVE_HC_CACHE_MODE); using SetPreferredAppMode_f = PreferredAppMode(WINAPI*)(PreferredAppMode); -static SetWindowCompositionAttribute_f SetWindowCompositionAttribute = nullptr; -static ShouldAppsUseDarkMode_f ShouldAppsUseDarkMode = nullptr; -static AllowDarkModeForWindow_f AllowDarkModeForWindow = nullptr; -static RefreshImmersiveColorPolicyState_f RefreshImmersiveColorPolicyState = - nullptr; -static IsDarkModeAllowedForWindow_f IsDarkModeAllowedForWindow = nullptr; -static GetIsImmersiveColorUsingHighContrast_f -GetIsImmersiveColorUsingHighContrast = nullptr; -static SetPreferredAppMode_f SetPreferredAppMode = nullptr; +static SetWindowCompositionAttribute_f setWindowCompositionAttribute = nullptr; +static ShouldAppsUseDarkMode_f shouldAppsUseDarkMode = nullptr; +static AllowDarkModeForWindow_f allowDarkModeForWindow = nullptr; +static RefreshImmersiveColorPolicyState_f refreshImmersiveColorPolicyState = nullptr; +static IsDarkModeAllowedForWindow_f isDarkModeAllowedForWindow = nullptr; +static GetIsImmersiveColorUsingHighContrast_f getIsImmersiveColorUsingHighContrast = nullptr; +static SetPreferredAppMode_f setPreferredAppMode = nullptr; -static constexpr DWORD WIN10_MINIMUM_BUILD_DARK_MODE = 18362; +static constexpr DWORD wiN10MinimumBuildDarkMode = 18362; -static std::once_flag flag_init_dark_mode_support; +static std::once_flag flagInitDarkModeSupport; namespace { class ModuleHandle { public: - ~ModuleHandle() { - if (_handle != nullptr) { - FreeLibrary(_handle); - } + ~ModuleHandle() { + if (_handle != nullptr) { + FreeLibrary(_handle); } + } - void reset(HMODULE handle) { - if (_handle != nullptr) { - FreeLibrary(_handle); - } - _handle = handle; + void reset(HMODULE handle) { + if (_handle != nullptr) { + FreeLibrary(_handle); } + _handle = handle; + } - HMODULE get() const { - return _handle; - } + auto get() const -> HMODULE { + return _handle; + } private: - HMODULE _handle = nullptr; + HMODULE _handle = nullptr; }; - ModuleHandle g_uxtheme; -} + ModuleHandle gUxtheme; +} // namespace static void EnableDarkModeForApp() noexcept { - if (SetPreferredAppMode != nullptr) { - SetPreferredAppMode(AllowDark); + if (setPreferredAppMode != nullptr) { + setPreferredAppMode(AllowDark); } } -[[nodiscard]] static DWORD GetBuildNumber() noexcept { - auto RtlGetNtVersionNumbers = - reinterpret_cast(GetProcAddress( - GetModuleHandleW(L"ntdll.dll"), "RtlGetNtVersionNumbers" - )); +[[nodiscard]] static auto GetBuildNumber() noexcept -> DWORD { + auto rtlGetNtVersionNumbers = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetNtVersionNumbers") + ); - if (RtlGetNtVersionNumbers == nullptr) { + if (rtlGetNtVersionNumbers == nullptr) { return 0; } DWORD major = 0; DWORD minor = 0; DWORD build = 0; - RtlGetNtVersionNumbers(&major, &minor, &build); + rtlGetNtVersionNumbers(&major, &minor, &build); build &= ~0xF0000000; return build; } -[[nodiscard]] static bool IsHighContrast() noexcept { - HIGHCONTRASTW high_contrast; - high_contrast.cbSize = sizeof(high_contrast); - if (SystemParametersInfoW( - SPI_GETHIGHCONTRAST, - sizeof(high_contrast), - &high_contrast, - FALSE - ) == TRUE) { - return (high_contrast.dwFlags & HCF_HIGHCONTRASTON) > 0; +[[nodiscard]] static auto IsHighContrast() noexcept -> bool { + HIGHCONTRASTW highContrast; + highContrast.cbSize = sizeof(highContrast); + if (SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(highContrast), &highContrast, FALSE) == TRUE) { + return (highContrast.dwFlags & HCF_HIGHCONTRASTON) > 0; } return false; } static void InitDarkModeSupportOnce() noexcept { - const auto build_number = GetBuildNumber(); + const auto buildNumber = GetBuildNumber(); - if (build_number < WIN10_MINIMUM_BUILD_DARK_MODE) { + if (buildNumber < wiN10MinimumBuildDarkMode) { return; } - g_uxtheme.reset( - LoadLibraryExW(L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) - ); + gUxtheme.reset(LoadLibraryExW(L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); - if (g_uxtheme.get() == nullptr) { + if (gUxtheme.get() == nullptr) { return; } - RefreshImmersiveColorPolicyState = - reinterpret_cast( - GetProcAddress(g_uxtheme.get(), MAKEINTRESOURCEA(104))); + refreshImmersiveColorPolicyState = + reinterpret_cast(GetProcAddress(gUxtheme.get(), MAKEINTRESOURCEA(104))); - GetIsImmersiveColorUsingHighContrast = - reinterpret_cast( - GetProcAddress(g_uxtheme.get(), MAKEINTRESOURCEA(106))); + getIsImmersiveColorUsingHighContrast = + reinterpret_cast(GetProcAddress(gUxtheme.get(), MAKEINTRESOURCEA(106))); - ShouldAppsUseDarkMode = reinterpret_cast( - GetProcAddress(g_uxtheme.get(), MAKEINTRESOURCEA(132))); + shouldAppsUseDarkMode = + reinterpret_cast(GetProcAddress(gUxtheme.get(), MAKEINTRESOURCEA(132))); - AllowDarkModeForWindow = reinterpret_cast( - GetProcAddress(g_uxtheme.get(), MAKEINTRESOURCEA(133))); + allowDarkModeForWindow = + reinterpret_cast(GetProcAddress(gUxtheme.get(), MAKEINTRESOURCEA(133))); - SetPreferredAppMode = reinterpret_cast( - GetProcAddress(g_uxtheme.get(), MAKEINTRESOURCEA(135))); + setPreferredAppMode = + reinterpret_cast(GetProcAddress(gUxtheme.get(), MAKEINTRESOURCEA(135))); - IsDarkModeAllowedForWindow = - reinterpret_cast( - GetProcAddress(g_uxtheme.get(), MAKEINTRESOURCEA(137))); + isDarkModeAllowedForWindow = + reinterpret_cast(GetProcAddress(gUxtheme.get(), MAKEINTRESOURCEA(137))); - SetWindowCompositionAttribute = - reinterpret_cast(GetProcAddress( - GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute" - )); + setWindowCompositionAttribute = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute") + ); - if (RefreshImmersiveColorPolicyState != nullptr && - ShouldAppsUseDarkMode != nullptr && - AllowDarkModeForWindow != nullptr && SetPreferredAppMode != nullptr && - IsDarkModeAllowedForWindow != nullptr) { + if (refreshImmersiveColorPolicyState != nullptr && shouldAppsUseDarkMode != nullptr && + allowDarkModeForWindow != nullptr && setPreferredAppMode != nullptr && isDarkModeAllowedForWindow != nullptr) { EnableDarkModeForApp(); - RefreshImmersiveColorPolicyState(); + refreshImmersiveColorPolicyState(); } } void InitDarkModeSupport() noexcept { - std::call_once(flag_init_dark_mode_support, InitDarkModeSupportOnce); + std::call_once(flagInitDarkModeSupport, InitDarkModeSupportOnce); } -bool IsDarkModeEnabled() noexcept { - if (ShouldAppsUseDarkMode == nullptr) { +auto IsDarkModeEnabled() noexcept -> bool { + if (shouldAppsUseDarkMode == nullptr) { return false; } - return (ShouldAppsUseDarkMode() == TRUE) && !IsHighContrast(); + return (shouldAppsUseDarkMode() == TRUE) && !IsHighContrast(); } void EnableDarkMode(const HWND hwnd, const bool enable) noexcept { - if (AllowDarkModeForWindow == nullptr) { + if (allowDarkModeForWindow == nullptr) { return; } - AllowDarkModeForWindow(hwnd, enable ? TRUE : FALSE); + allowDarkModeForWindow(hwnd, enable ? TRUE : FALSE); } void RefreshNonClientArea(const HWND hwnd) noexcept { - if (IsDarkModeAllowedForWindow == nullptr || - ShouldAppsUseDarkMode == nullptr) { + if (isDarkModeAllowedForWindow == nullptr || shouldAppsUseDarkMode == nullptr) { return; } BOOL dark = FALSE; - if (IsDarkModeAllowedForWindow(hwnd) == TRUE && - ShouldAppsUseDarkMode() == TRUE && !IsHighContrast()) { + if (isDarkModeAllowedForWindow(hwnd) == TRUE && shouldAppsUseDarkMode() == TRUE && !IsHighContrast()) { dark = TRUE; } - if (SetWindowCompositionAttribute != nullptr) { - WINDOWCOMPOSITIONATTRIBDATA data = { - WCA_USEDARKMODECOLORS, - &dark, - sizeof(dark) - }; - SetWindowCompositionAttribute(hwnd, &data); + if (setWindowCompositionAttribute != nullptr) { + WINDOWCOMPOSITIONATTRIBDATA data = {WCA_USEDARKMODECOLORS, &dark, sizeof(dark)}; + setWindowCompositionAttribute(hwnd, &data); } } -bool IsColorSchemeChange(const LPARAM l_param) noexcept { - bool return_value = false; - if (l_param > 0 && CompareStringOrdinal( - reinterpret_cast(l_param), - -1, - L"ImmersiveColorSet", - -1, - TRUE - ) == CSTR_EQUAL) { - if (RefreshImmersiveColorPolicyState != nullptr) { - RefreshImmersiveColorPolicyState(); +auto IsColorSchemeChange(const LPARAM lParam) noexcept -> bool { + bool returnValue = false; + if (lParam > 0 && + CompareStringOrdinal(reinterpret_cast(lParam), -1, L"ImmersiveColorSet", -1, TRUE) == CSTR_EQUAL) { + if (refreshImmersiveColorPolicyState != nullptr) { + refreshImmersiveColorPolicyState(); } - return_value = true; + returnValue = true; } - if (GetIsImmersiveColorUsingHighContrast != nullptr) { - GetIsImmersiveColorUsingHighContrast(IHCM_REFRESH); + if (getIsImmersiveColorUsingHighContrast != nullptr) { + getIsImmersiveColorUsingHighContrast(IHCM_REFRESH); } - return return_value; + return returnValue; } diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/DarkMode.h b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/DarkMode.h index 65c0cd3ee..d74616e04 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/DarkMode.h +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/DarkMode.h @@ -1,14 +1,11 @@ #pragma once -/** - * @file DarkMode.h - * @brief Win32 dark-mode helpers using undocumented UxTheme APIs - * - * Provides runtime detection and application of Windows dark mode for the - * non-client area (title bar, borders). All functions are noexcept and safe - * to call even when the underlying APIs are unavailable - */ - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #include +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- /** @brief Detect available dark-mode APIs at runtime and cache the results. Must be called once at startup */ void InitDarkModeSupport() noexcept; @@ -17,7 +14,7 @@ void InitDarkModeSupport() noexcept; * @brief Check whether the current Windows theme is dark * @return true if the system is in dark mode */ -[[nodiscard]] bool IsDarkModeEnabled() noexcept; +[[nodiscard]] auto IsDarkModeEnabled() noexcept -> bool; /** * @brief Apply or remove dark mode coloring on a window's non-client area @@ -38,7 +35,7 @@ void RefreshNonClientArea(HWND hwnd) noexcept; * @param l_param lParam from a WM_SETTINGCHANGE message * @return true if the message indicates an immersive color-scheme change */ -[[nodiscard]] bool IsColorSchemeChange(LPARAM l_param) noexcept; +[[nodiscard]] auto IsColorSchemeChange(LPARAM lParam) noexcept -> bool; // --------------------------------------------------------------------------------------------------------------------- // Internal UxTheme / DWM types (undocumented Win32 API surface) @@ -48,48 +45,48 @@ void RefreshNonClientArea(HWND hwnd) noexcept; /** @brief Controls whether the immersive color cache is used or refreshed */ enum IMMERSIVE_HC_CACHE_MODE { IHCM_USE_CACHED_VALUE = 0, /// Use the previously cached value - IHCM_REFRESH = 1, /// Force a refresh of the cached value + IHCM_REFRESH = 1, /// Force a refresh of the cached value }; /** @brief Application color-mode preference passed to SetPreferredAppMode */ enum PreferredAppMode { - Default = 0, /// Follow the system setting - AllowDark = 1, /// Allow dark mode if the system is dark - ForceDark = 2, /// Always use dark mode + Default = 0, /// Follow the system setting + AllowDark = 1, /// Allow dark mode if the system is dark + ForceDark = 2, /// Always use dark mode ForceLight = 3, /// Always use light mode - Max = 4, /// Sentinel value; not a valid mode + Max = 4, /// Sentinel value; not a valid mode }; /** @brief Window composition attribute identifiers used with SetWindowCompositionAttribute */ enum WINDOWCOMPOSITIONATTRIB { - WCA_UNDEFINED = 0, - WCA_NCRENDERING_ENABLED = 1, /// Non-client rendering enabled flag - WCA_NCRENDERING_POLICY = 2, /// Non-client rendering policy - WCA_TRANSITIONS_FORCEDISABLED = 3, - WCA_ALLOW_NCPAINT = 4, - WCA_CAPTION_BUTTON_BOUNDS = 5, - WCA_NONCLIENT_RTL_LAYOUT = 6, - WCA_FORCE_ICONIC_REPRESENTATION = 7, - WCA_EXTENDED_FRAME_BOUNDS = 8, - WCA_HAS_ICONIC_BITMAP = 9, - WCA_THEME_ATTRIBUTES = 10, - WCA_NCRENDERING_EXILED = 11, - WCA_NCADORNMENTINFO = 12, - WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, - WCA_VIDEO_OVERLAY_ACTIVE = 14, + WCA_UNDEFINED = 0, + WCA_NCRENDERING_ENABLED = 1, /// Non-client rendering enabled flag + WCA_NCRENDERING_POLICY = 2, /// Non-client rendering policy + WCA_TRANSITIONS_FORCEDISABLED = 3, + WCA_ALLOW_NCPAINT = 4, + WCA_CAPTION_BUTTON_BOUNDS = 5, + WCA_NONCLIENT_RTL_LAYOUT = 6, + WCA_FORCE_ICONIC_REPRESENTATION = 7, + WCA_EXTENDED_FRAME_BOUNDS = 8, + WCA_HAS_ICONIC_BITMAP = 9, + WCA_THEME_ATTRIBUTES = 10, + WCA_NCRENDERING_EXILED = 11, + WCA_NCADORNMENTINFO = 12, + WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, + WCA_VIDEO_OVERLAY_ACTIVE = 14, WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, - WCA_DISALLOW_PEEK = 16, - WCA_CLOAK = 17, - WCA_CLOAKED = 18, - WCA_ACCENT_POLICY = 19, - WCA_FREEZE_REPRESENTATION = 20, - WCA_EVER_UNCLOAKED = 21, - WCA_VISUAL_OWNER = 22, - WCA_HOLOGRAPHIC = 23, - WCA_EXCLUDED_FROM_DDA = 24, - WCA_PASSIVEUPDATEMODE = 25, - WCA_USEDARKMODECOLORS = 26, /// Enable dark mode colors for non-client area - WCA_LAST = 27, + WCA_DISALLOW_PEEK = 16, + WCA_CLOAK = 17, + WCA_CLOAKED = 18, + WCA_ACCENT_POLICY = 19, + WCA_FREEZE_REPRESENTATION = 20, + WCA_EVER_UNCLOAKED = 21, + WCA_VISUAL_OWNER = 22, + WCA_HOLOGRAPHIC = 23, + WCA_EXCLUDED_FROM_DDA = 24, + WCA_PASSIVEUPDATEMODE = 25, + WCA_USEDARKMODECOLORS = 26, /// Enable dark mode colors for non-client area + WCA_LAST = 27, }; /** @brief Parameter struct for SetWindowCompositionAttribute */ diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Dialog.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Dialog.cpp index 6a9d481dd..299956cf1 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Dialog.cpp +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Dialog.cpp @@ -1,16 +1,16 @@ -/** - * @file Dialog.cpp (Windows) - * @brief Windows implementation of InfiniFrameDialog using IFileDialog (Vista+) and MessageBoxW - */ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/InfiniFrame.h" -#include "Core/InfiniFrame.h" - -#include #include #include #include #include #include +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- /** * @brief RAII wrapper that loads a DLL on construction and frees it on destruction. @@ -18,48 +18,45 @@ */ class Dll { public: - /** @brief Load the named DLL; handle is null if loading fails */ - explicit Dll(const std::string& name); - /** @brief Unload the DLL if it was loaded successfully */ - ~Dll(); + /** @brief Load the named DLL; handle is null if loading fails */ + explicit Dll(const std::string& name); + /** @brief Unload the DLL if it was loaded successfully */ + ~Dll(); - /** + /** * @brief Type-safe wrapper around a single exported function retrieved via GetProcAddress * @tparam T Function signature (e.g. BOOL(HWND, LPCWSTR)) */ - template - class Proc { - public: - /** + template class Proc { + public: + /** * @brief Resolve a symbol from a loaded DLL * @param lib DLL to search * @param sym Exported symbol name */ - Proc(const Dll& lib, const std::string& sym) : - _mProc(static_cast(reinterpret_cast(GetProcAddress(lib._handle, sym.c_str())))) { - } + Proc(const Dll& lib, const std::string& sym) + : _mProc(static_cast(reinterpret_cast(GetProcAddress(lib._handle, sym.c_str())))) {} - /** @brief Returns true if the symbol was resolved successfully */ - explicit operator bool() const { - return _mProc != nullptr; - } + /** @brief Returns true if the symbol was resolved successfully */ + explicit operator bool() const { + return _mProc != nullptr; + } - /** @brief Returns the raw function pointer */ - explicit operator T*() const { - return _mProc; - } + /** @brief Returns the raw function pointer */ + explicit operator T*() const { + return _mProc; + } - private: - T* _mProc; - }; + private: + T* _mProc; + }; private: - HMODULE _handle; + HMODULE _handle; }; -inline Dll::Dll(const std::string& name) : - _handle(LoadLibraryA(name.c_str())) { -} +inline Dll::Dll(const std::string& name) + : _handle(LoadLibraryA(name.c_str())) {} inline Dll::~Dll() { if (_handle) @@ -75,25 +72,25 @@ inline Dll::~Dll() { */ class NewStyleContext { public: - /** @brief Activate the Common Controls v6 context */ - NewStyleContext(); - /** @brief Deactivate the context */ - ~NewStyleContext(); + /** @brief Activate the Common Controls v6 context */ + NewStyleContext(); + /** @brief Deactivate the context */ + ~NewStyleContext(); private: - /** @brief Create the activation context from shell32.dll's manifest; called once */ - static HANDLE Create(); + /** @brief Create the activation context from shell32.dll's manifest; called once */ + static HANDLE Create(); - struct ActivationContextHolder { - HANDLE handle = INVALID_HANDLE_VALUE; + struct ActivationContextHolder { + HANDLE handle = INVALID_HANDLE_VALUE; - ~ActivationContextHolder() { - if (handle != INVALID_HANDLE_VALUE) - ReleaseActCtx(handle); - } - }; + ~ActivationContextHolder() { + if (handle != INVALID_HANDLE_VALUE) + ReleaseActCtx(handle); + } + }; - ULONG_PTR _cookie = 0; /// Activation cookie returned by ActivateActCtx; used to deactivate + ULONG_PTR _cookie = 0; /// Activation cookie returned by ActivateActCtx; used to deactivate }; inline NewStyleContext::NewStyleContext() { @@ -115,8 +112,7 @@ inline HANDLE NewStyleContext::Create() { std::string sysDir(len, '\0'); GetSystemDirectoryA(const_cast(sysDir.data()), len); - const ACTCTXA actCtx = - { + const ACTCTXA actCtx = { sizeof(actCtx), ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, "shell32.dll", @@ -148,15 +144,12 @@ InfiniFrameDialog::~InfiniFrameDialog() { * @param defaultPath UTF-16 path to pre-select as the starting folder; may be null * @return Pointer to the created dialog; caller owns the COM reference. Returns null on failure. */ -template -T* Create(HRESULT* hResult, AutoStringConst title, const AutoStringConst defaultPath) { +template T* Create(HRESULT* hResult, AutoStringConst title, const AutoStringConst defaultPath) { static_assert(std::is_base_of::value, "T must inherit from IFileDialog"); T* pfd = nullptr; - const CLSID clsid = typeid(T) == typeid(IFileOpenDialog) - ? CLSID_FileOpenDialog - : typeid(T) == typeid(IFileSaveDialog) - ? CLSID_FileSaveDialog - : CLSID_FileOpenDialog; + const CLSID clsid = typeid(T) == typeid(IFileOpenDialog) ? CLSID_FileOpenDialog + : typeid(T) == typeid(IFileSaveDialog) ? CLSID_FileSaveDialog + : CLSID_FileOpenDialog; HRESULT hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd)); if (SUCCEEDED(hr)) { pfd->SetTitle(title); @@ -195,7 +188,7 @@ void AddFilters( const int filterCount, InfiniFrameWindow* wndInstance, std::vector& filterStorage - ) { +) { std::vector specs; for (int i = 0; i < filterCount; i++) { filterStorage.push_back(wndInstance->ToUTF16String(filters[i])); @@ -270,7 +263,7 @@ AutoString* InfiniFrameDialog::ShowOpenFile( AutoString* filters, const int filterCount, int* resultCount - ) { +) { HRESULT hr; std::wstring wideTitle = _window->ToUTF16String(title); std::wstring wideDefaultPath = _window->ToUTF16String(defaultPath); @@ -286,8 +279,7 @@ AutoString* InfiniFrameDialog::ShowOpenFile( dwOptions |= FOS_FILEMUSTEXIST | FOS_NOCHANGEDIR; if (multiSelect) { dwOptions |= FOS_ALLOWMULTISELECT; - } - else { + } else { dwOptions &= ~FOS_ALLOWMULTISELECT; } pfd->SetOptions(dwOptions); @@ -302,11 +294,8 @@ AutoString* InfiniFrameDialog::ShowOpenFile( } AutoString* InfiniFrameDialog::ShowOpenFolder( - AutoString title, - AutoString defaultPath, - const bool multiSelect, - int* resultCount - ) { + AutoString title, AutoString defaultPath, const bool multiSelect, int* resultCount +) { HRESULT hr; std::wstring wideTitle = _window->ToUTF16String(title); std::wstring wideDefaultPath = _window->ToUTF16String(defaultPath); @@ -319,8 +308,7 @@ AutoString* InfiniFrameDialog::ShowOpenFolder( dwOptions |= FOS_PICKFOLDERS | FOS_NOCHANGEDIR; if (multiSelect) { dwOptions |= FOS_ALLOWMULTISELECT; - } - else { + } else { dwOptions &= ~FOS_ALLOWMULTISELECT; } pfd->SetOptions(dwOptions); @@ -335,12 +323,8 @@ AutoString* InfiniFrameDialog::ShowOpenFolder( } AutoString InfiniFrameDialog::ShowSaveFile( - AutoString title, - AutoString defaultPath, - AutoString* filters, - const int filterCount, - AutoString defaultFileName - ) { + AutoString title, AutoString defaultPath, AutoString* filters, const int filterCount, AutoString defaultFileName +) { HRESULT hr; std::wstring wideTitle = _window->ToUTF16String(title); std::wstring wideDefaultPath = _window->ToUTF16String(defaultPath); @@ -384,11 +368,8 @@ AutoString InfiniFrameDialog::ShowSaveFile( } DialogResult InfiniFrameDialog::ShowMessage( - AutoString title, - AutoString text, - const DialogButtons buttons, - const DialogIcon icon - ) { + AutoString title, AutoString text, const DialogButtons buttons, const DialogIcon icon +) { std::wstring wideTitle = _window->ToUTF16String(title); std::wstring wideText = _window->ToUTF16String(text); NewStyleContext ctx; diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/ToastHandler.h b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/ToastHandler.h index 7b8c2afff..17b87b40c 100644 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/ToastHandler.h +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/ToastHandler.h @@ -1,13 +1,14 @@ #pragma once -/** - * @file ToastHandler.h - * @brief WinToast event handler that brings the window to the foreground on notification interaction - */ - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #include -#include "Core/InfiniFrameWindow.h" -#include "Dependencies/wintoastlib/wintoastlib.h" +#include "Public/InfiniFrameWindow.h" +#include "Dependencies/wintoastlib/wintoastlib.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- using namespace WinToastLib; /** @@ -21,45 +22,42 @@ class WinToastHandler final : public IWinToastHandler { InfiniFrameWindow* _window; public: - /** + /** * @brief Construct a handler bound to a specific window * @param window The window to bring to the foreground on notification activation */ - explicit WinToastHandler(InfiniFrameWindow* window) : - _window(window) { - } + explicit WinToastHandler(InfiniFrameWindow* window) + : _window(window) {} - /** @brief Called when the user clicks the notification body; restores and focuses the window */ - void toastActivated() const override { - ShowWindow(this->_window->getHwnd(), SW_SHOW); - ShowWindow(this->_window->getHwnd(), SW_RESTORE); - SetForegroundWindow(this->_window->getHwnd()); - } + /** @brief Called when the user clicks the notification body; restores and focuses the window */ + void toastActivated() const override { + ShowWindow(this->_window->getHwnd(), SW_SHOW); + ShowWindow(this->_window->getHwnd(), SW_RESTORE); + SetForegroundWindow(this->_window->getHwnd()); + } - /** + /** * @brief Called when the user clicks an action button on the notification * @param actionIndex Zero-based index of the activated button (unused; delegates to toastActivated()) */ - void toastActivated(int) const override { - toastActivated(); - } + void toastActivated(int) const override { + toastActivated(); + } - /** + /** * @brief Called when the user submits a text-input reply on the notification * @param response User-entered text (unused; delegates to toastActivated()) */ - void toastActivated(std::wstring) const override { - toastActivated(); - } + void toastActivated(std::wstring) const override { + toastActivated(); + } - /** + /** * @brief Called when the notification is dismissed without activation * @param state Reason for dismissal (timeout, user swipe, app hide, etc.) */ - void toastDismissed(WinToastDismissalReason) const override { - } + void toastDismissed(WinToastDismissalReason) const override {} - /** @brief Called when the notification fails to display */ - void toastFailed() const override { - } + /** @brief Called when the notification fails to display */ + void toastFailed() const override {} }; diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Attach.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Attach.Win32.cpp new file mode 100644 index 000000000..7147afcd4 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Attach.Win32.cpp @@ -0,0 +1,483 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include + +#include "Embedded/Embedded.h" +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +using namespace Microsoft::WRL; + +void InfiniFrameWindow::Show(const bool isAlreadyShown) { + if (!isAlreadyShown) + ShowWindow(m_impl->_hWnd, SW_SHOWDEFAULT); + + UpdateWindow(m_impl->_hWnd); + + if (!m_impl->_webviewController) { + bool hasConfiguredRuntimePath = false; + { + std::lock_guard lock(webview2RuntimePathMutex); + hasConfiguredRuntimePath = wcsnlen(_webview2RuntimePath, _countof(_webview2RuntimePath)) > 0; + } + if (hasConfiguredRuntimePath || EnsureWebViewIsInstalled()) + AttachWebView(); + else + exit(0); + } +} + +// Initializes and attaches the WebView2 instance to this window. +// +// Responsibility: +// - Perform one-shot WebView2 initialization for this native window instance. +// - Create environment/controller, apply settings, and wire all required callbacks. +// - Leave the object in a consistent state when initialization fails or is aborted. +// +// High-level flow: +// 1) Bail out if the window is closing/closed, or if initialization already started/completed. +// 2) Resolve optional runtime path under lock (host-configurable global state). +// 3) Build browser startup arguments from feature flags/host parameters. +// 4) Create WebView2 environment and controller asynchronously. +// 5) Configure WebView and subscribe event handlers (navigation, messaging, permissions, etc.). +// 6) Finalize initialized flags on success; clear initializing flag on all exit paths. +// +// Notes for maintainers: +// - This function is intentionally stateful and order-sensitive; guard checks must remain first. +// - `_isWebView2Initializing` prevents duplicate concurrent initialization. +// - Async callbacks depend on stable captured values; do not convert locked snapshots to borrowed refs. +void InfiniFrameWindow::AttachWebView() { + // Guard: no attachment work should run after close has been requested. + if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) + return; + + // Guard: avoid concurrent or repeated initialization. + if (m_impl->_isWebView2Initializing || m_impl->_isInitialized) + return; + m_impl->_isWebView2Initializing = true; + + // Snapshot runtime path under lock so subsequent async setup uses a stable value. + std::wstring configuredRuntimePath; + { + std::lock_guard lock(webview2RuntimePathMutex); + configuredRuntimePath = _webview2RuntimePath; + } + PCWSTR runtimePath = configuredRuntimePath.empty() ? nullptr : configuredRuntimePath.c_str(); + + // Compose WebView2 command-line switches from current window/browser options. + // This string is passed to environment creation and controls browser process behavior. + 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 " + L"WebView2 Runtime to a version that supports ICoreWebView2EnvironmentOptions4.", + L"WebView2 Runtime Too Old", MB_OK | MB_ICONERROR + ); + m_impl->_isWebView2Initializing = false; + return; + } + + PCWSTR userDataPath = nullptr; + if (!m_impl->_temporaryFilesPath.empty()) { + if (EnsureDirectoryWritable(m_impl->_temporaryFilesPath)) + userDataPath = m_impl->_temporaryFilesPath.c_str(); + else + TraceTeardown( + L"AttachWebView: temporary user-data path is not writable. Falling back to default path. path=%ls", + m_impl->_temporaryFilesPath.c_str() + ); + } + + HRESULT envResult = CreateCoreWebView2EnvironmentWithOptions( + runtimePath, userDataPath, options.Get(), + Callback( + [this](const HRESULT result, ICoreWebView2Environment* env) -> HRESULT { + if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) { + m_impl->_isWebView2Initializing = false; + m_impl->_webviewEnvironment = nullptr; + TraceTeardown(L"CreateEnvironment callback while closing; ignoring"); + return S_OK; + } + if (result != S_OK) { + m_impl->_isWebView2Initializing = false; + TraceTeardown(L"CreateEnvironment callback failed hr=0x%08X", static_cast(result)); + return result; + } + if (env == nullptr) { + m_impl->_isWebView2Initializing = false; + return E_POINTER; + } + HRESULT envResult = env->QueryInterface(&m_impl->_webviewEnvironment); + if (envResult != S_OK) { + m_impl->_isWebView2Initializing = false; + return envResult; + } + + const HRESULT createControllerHr = env->CreateCoreWebView2Controller( + m_impl->_hWnd, + Callback( + [this](const HRESULT result, ICoreWebView2Controller* controller) -> HRESULT { + if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) { + if (controller != nullptr) + controller->Close(); + m_impl->_webviewController = nullptr; + m_impl->_webviewWindow = nullptr; + m_impl->_webviewEnvironment = nullptr; + m_impl->_isWebView2Initializing = false; + TraceTeardown(L"CreateController callback while closing; ignoring"); + return S_OK; + } + if (result != S_OK) { + m_impl->_isWebView2Initializing = false; + TraceTeardown( + L"CreateController callback failed hr=0x%08X", static_cast(result) + ); + return result; + } + if (controller == nullptr) { + m_impl->_isWebView2Initializing = false; + return E_POINTER; + } + + HRESULT envResult = controller->QueryInterface(&m_impl->_webviewController); + if (envResult != S_OK) { + m_impl->_isWebView2Initializing = false; + return envResult; + } + m_impl->_webviewController->get_CoreWebView2(&m_impl->_webviewWindow); + if (!m_impl->_webviewWindow) { + m_impl->_isWebView2Initializing = false; + return E_FAIL; + } + + const auto js_wide = Embedded::InfiniFrameJsUtf16(); + OutputDebugStringW( + std::format(L"[InfiniFrame] Bridge script length: {} chars\n", js_wide.size()).c_str() + ); + + 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}); + + 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( + [this](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT { + if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) + 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') && + m_impl->_webviewWindow != nullptr) { + m_impl->_webviewWindow->get_Source(&source); + } + m_impl->_webMessageReceivedCallback(message.get(), source.get()); + return S_OK; + } + ).Get(), + &webMessageToken + ); + m_impl->_webMessageReceivedToken = webMessageToken; + m_impl->_hasWebMessageReceivedToken = true; + + 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( + [this](ICoreWebView2*, ICoreWebView2WebResourceRequestedEventArgs* args) { + if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) + return S_OK; + + wil::com_ptr 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 + ); + m_impl->_webResourceRequestedTokenForCustomScheme = webResourceRequestedToken; + m_impl->_hasWebResourceRequestedToken = true; + + EventRegistrationToken permissionRequestedToken; + m_impl->_webviewWindow->add_PermissionRequested( + Callback( + [this](ICoreWebView2*, ICoreWebView2PermissionRequestedEventArgs* args) -> HRESULT { + if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) + return S_OK; + + if (m_impl->_grantBrowserPermissions) + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + return S_OK; + } + ).Get(), + &permissionRequestedToken + ); + m_impl->_permissionRequestedToken = permissionRequestedToken; + m_impl->_hasPermissionRequestedToken = true; + + 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); + + HRESULT addScriptHr = m_impl->_webviewWindow->AddScriptToExecuteOnDocumentCreated( + js_wide.c_str(), + Callback( + [nav, this](HRESULT errorCode, LPCWSTR id) -> HRESULT { + OutputDebugStringW( + std::format( + L"[InfiniFrame] AddScriptToExecuteOnDocumentCreated callback: " + L"hr=0x{:08X} id={}\n", + (unsigned)errorCode, id ? id : L"(null)" + ) + .c_str() + ); + if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) + return S_OK; + nav->navigate(); + return S_OK; + } + ).Get() + ); + + if (FAILED(addScriptHr)) + nav->navigate(); + + RefitContent(); + FocusWebView2(); + + if (m_impl->_topmost) + SetTopmost(true); + + m_impl->_isInitialized = true; + m_impl->_isWebView2Initializing = false; + return S_OK; + } + ).Get() + ); + if (FAILED(createControllerHr)) + m_impl->_isWebView2Initializing = false; + + return createControllerHr; + } + ).Get() + ); + + if (envResult != S_OK) { + m_impl->_isWebView2Initializing = false; + _com_error err(envResult); + LPCTSTR errMsg = err.ErrorMessage(); + MessageBox(m_impl->_hWnd, errMsg, L"Error instantiating webview", MB_OK); + } +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Controller.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Controller.Win32.cpp new file mode 100644 index 000000000..25b25a07b --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Controller.Win32.cpp @@ -0,0 +1,52 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +using namespace Microsoft::WRL; + +void InfiniFrameWindow::RefitContent() { + if (m_impl->_webviewController) { + RECT bounds; + GetClientRect(m_impl->_hWnd, &bounds); + m_impl->_webviewController->put_Bounds(bounds); + } +} + +void InfiniFrameWindow::FocusWebView2() { + if (m_impl->_webviewController) { + m_impl->_webviewController->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); + } +} + +void InfiniFrameWindow::NotifyWebView2WindowMove() { + if (m_impl->_webviewController) { + m_impl->_webviewController->NotifyParentWindowPositionChanged(); + } +} + +void InfiniFrameWindow::ClearBrowserAutoFill() { + if (!m_impl->_webviewWindow) + return; + + auto webview15 = m_impl->_webviewWindow.try_query(); + if (webview15) { + wil::com_ptr profile; + webview15->get_Profile(&profile); + auto profile2 = profile.try_query(); + + if (profile2) { + COREWEBVIEW2_BROWSING_DATA_KINDS dataKinds = + (COREWEBVIEW2_BROWSING_DATA_KINDS)(COREWEBVIEW2_BROWSING_DATA_KINDS_GENERAL_AUTOFILL | + COREWEBVIEW2_BROWSING_DATA_KINDS_PASSWORD_AUTOSAVE); + + profile2->ClearBrowsingData( + dataKinds, Callback([this](HRESULT) -> HRESULT { + return S_OK; + }).Get() + ); + } + } +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Host.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Host.Win32.cpp new file mode 100644 index 000000000..2027b475a --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Host.Win32.cpp @@ -0,0 +1,53 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +void InfiniFrameWindow::CloseWebView() { + m_impl->_isClosingOrClosed.store(true, std::memory_order_release); + const bool deferEnvironmentRelease = m_impl->_isWebView2Initializing && m_impl->_webviewController == nullptr; + TraceTeardown( + L"CloseWebView begin instance=%p hwnd=%p controller=%p webview=%p env=%p", this, m_impl->_hWnd, + m_impl->_webviewController.get(), m_impl->_webviewWindow.get(), m_impl->_webviewEnvironment.get() + ); + + if (m_impl->_webviewController != nullptr) { + m_impl->_webviewController->Close(); + m_impl->_webviewController = nullptr; + } + + m_impl->_webviewWindow = nullptr; + + m_impl->_hasWebMessageReceivedToken = false; + m_impl->_hasWebResourceRequestedToken = false; + m_impl->_hasPermissionRequestedToken = false; + m_impl->_webMessageReceivedToken = {}; + m_impl->_webResourceRequestedTokenForCustomScheme = {}; + m_impl->_permissionRequestedToken = {}; + + if (m_impl->_webviewEnvironment != nullptr && !deferEnvironmentRelease) { + m_impl->_webviewEnvironment = nullptr; + } + + m_impl->_isInitialized = false; + if (!deferEnvironmentRelease) + m_impl->_isWebView2Initializing = false; + + if (deferEnvironmentRelease) { + TraceTeardown( + L"CloseWebView deferring environment release instance=%p env=%p", this, m_impl->_webviewEnvironment.get() + ); + } + + TraceTeardown(L"CloseWebView end instance=%p", this); +} + +std::string InfiniFrameWindow::ToUTF8String(const AutoString source) const { + return WideToUtf8(source); +} + +std::wstring InfiniFrameWindow::ToUTF16String(const AutoString source) const { + return Utf8ToWide(source); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Runtime.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Runtime.Win32.cpp new file mode 100644 index 000000000..a0c6320a6 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/WebView/WebView2Runtime.Win32.cpp @@ -0,0 +1,58 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#pragma comment(lib, "Urlmon.lib") +#include + +#include "Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +bool InfiniFrameWindow::EnsureWebViewIsInstalled() { + LPWSTR versionInfo = nullptr; + HRESULT ensureInstalledResult = GetAvailableCoreWebView2BrowserVersionString(nullptr, &versionInfo); + if (versionInfo != nullptr) + CoTaskMemFree(versionInfo); + + if (ensureInstalledResult != S_OK) + return InstallWebView2(); + + return true; +} + +bool InfiniFrameWindow::InstallWebView2() { + auto srcURL = L"https://go.microsoft.com/fwlink/p/?LinkId=2124703"; + auto destFile = L"MicrosoftEdgeWebview2Setup.exe"; + + if (S_OK == URLDownloadToFile(nullptr, srcURL, destFile, 0, nullptr)) { + std::wstring command = L"MicrosoftEdgeWebview2Setup.exe"; + + STARTUPINFO si; + PROCESS_INFORMATION pi; + + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + bool success = CreateProcess(nullptr, command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi); + + if (success) { + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + + return success; + } + + return false; +} + +void InfiniFrameWindow::SetWebView2RuntimePath(const AutoString pathToWebView2) { + if (pathToWebView2 == nullptr) + return; + + std::wstring widePath = Utf8ToWide(pathToWebView2); + std::lock_guard lock(webview2RuntimePathMutex); + wcsncpy_s(_webview2RuntimePath, widePath.c_str(), _countof(_webview2RuntimePath)); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.Win32.Context.h b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.Win32.Context.h new file mode 100644 index 000000000..38fb3bd89 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.Win32.Context.h @@ -0,0 +1,78 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include +#include +#include + +#include + +#include "Public/InfiniFrameWindow.h" +#include "Platform/Windows/Window.Win32.Internal.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +inline constexpr UINT WM_USER_INVOKE = WM_USER + 0x0002; + +extern std::atomic _hInstance; +extern thread_local HWND messageLoopRootWindowHandle; +extern wchar_t _webview2RuntimePath[MAX_PATH]; +extern std::mutex webview2RuntimePathMutex; +extern const wchar_t* CLASS_NAME; + +struct InvokeWaitInfo { + std::mutex mutex; + std::condition_variable completionNotifier; + bool isCompleted = false; + bool isAbandoned = false; +}; + +bool IsTeardownTraceEnabled(); +void TraceTeardown(const wchar_t* format, ...); +std::wstring Utf8ToWide(AutoString source); +std::string WideToUtf8(AutoString source); +bool EnsureDirectoryWritable(const std::wstring& directoryPath); +InfiniFrameWindow* LookupWindowInstance(HWND hwnd); +HWND ResolveParentWindowHandle(InfiniFrameWindow* parent); +HBRUSH GetDarkBrush(); +HBRUSH GetLightBrush(); + +template void ApplyPendingOwnerWindow(TImpl* impl, const wchar_t* phase) { + if (impl == nullptr) + return; + if (impl->_ownerAssigned) + return; + + if (impl->_pendingOwnerHwnd == nullptr || impl->_hWnd == nullptr) + return; + + if (impl->_pendingOwnerHwnd == impl->_hWnd) + return; + + if (!IsWindow(impl->_pendingOwnerHwnd) || !IsWindow(impl->_hWnd)) + return; + + SetLastError(0); + const LONG_PTR previousOwner = + SetWindowLongPtr(impl->_hWnd, GWLP_HWNDPARENT, reinterpret_cast(impl->_pendingOwnerHwnd)); + const DWORD lastError = GetLastError(); + + if (previousOwner == 0 && lastError != 0) { + TraceTeardown( + L"ApplyPendingOwnerWindow failed phase=%ls child=%p owner=%p err=%lu", phase, impl->_hWnd, + impl->_pendingOwnerHwnd, lastError + ); + return; + } + + impl->_ownerAssigned = true; + + const DWORD childThreadId = GetWindowThreadProcessId(impl->_hWnd, nullptr); + const DWORD ownerThreadId = GetWindowThreadProcessId(impl->_pendingOwnerHwnd, nullptr); + TraceTeardown( + L"ApplyPendingOwnerWindow success phase=%ls child=%p owner=%p childTid=%lu ownerTid=%lu prev=%p", phase, + impl->_hWnd, impl->_pendingOwnerHwnd, childThreadId, ownerThreadId, reinterpret_cast(previousOwner) + ); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.Win32.Internal.h b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.Win32.Internal.h new file mode 100644 index 000000000..0281eb3ca --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.Win32.Internal.h @@ -0,0 +1,66 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include + +#include +#include +#include + +#include "Public/InfiniFrameWindow.h" +#include "Public/InfiniFrameWindowImpl.h" +#include "Platform/Windows/ToastHandler.h" +#include "Utils/Common.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- + +struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { + std::wstring _temporaryFilesPath; + std::wstring _notificationRegistrationId; + + bool _notificationsEnabled = false; + bool _isInitialized = false; + bool _isWebView2Initializing = false; + std::atomic _isClosingOrClosed = 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; + HWND _pendingOwnerHwnd = nullptr; + bool _ownerAssigned = false; + wil::com_ptr _webviewController; + wil::com_ptr _webviewWindow; + wil::com_ptr _webviewEnvironment; + + EventRegistrationToken _webMessageReceivedToken = {}; + EventRegistrationToken _webResourceRequestedTokenForCustomScheme = {}; + EventRegistrationToken _permissionRequestedToken = {}; + EventRegistrationToken _windowClosedToken = {}; + EventRegistrationToken _windowClosingToken = {}; + EventRegistrationToken _documentTitleChangedToken = {}; + EventRegistrationToken _coreWebView2InitializedToken = {}; + bool _hasWebMessageReceivedToken = false; + bool _hasWebResourceRequestedToken = false; + bool _hasPermissionRequestedToken = false; + + std::unique_ptr _toastHandler; +}; diff --git a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.cpp b/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.cpp deleted file mode 100644 index df8950b63..000000000 --- a/src/InfiniFrame.NativeBridge/Native/Platform/Windows/Window.cpp +++ /dev/null @@ -1,2262 +0,0 @@ -#include -#include -#include -#include -#include -#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 "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; - std::atomic _isClosingOrClosed = 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; - HWND _pendingOwnerHwnd = nullptr; - bool _ownerAssigned = false; - wil::com_ptr _webviewController; - wil::com_ptr _webviewWindow; - wil::com_ptr _webviewEnvironment; - - EventRegistrationToken _webMessageReceivedToken = {}; - EventRegistrationToken _webResourceRequestedTokenForCustomScheme = {}; - EventRegistrationToken _permissionRequestedToken = {}; - EventRegistrationToken _windowClosedToken = {}; - EventRegistrationToken _windowClosingToken = {}; - EventRegistrationToken _documentTitleChangedToken = {}; - EventRegistrationToken _coreWebView2InitializedToken = {}; - bool _hasWebMessageReceivedToken = false; - bool _hasWebResourceRequestedToken = false; - bool _hasPermissionRequestedToken = false; - - std::unique_ptr _toastHandler; -}; - -LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); -auto CLASS_NAME = L"InfiniFrame"; -std::atomic _hInstance{nullptr}; -thread_local HWND messageLoopRootWindowHandle = nullptr; -wchar_t _webview2RuntimePath[MAX_PATH]; -std::mutex webview2RuntimePathMutex; - -namespace { - static_assert(sizeof(wchar_t) == sizeof(char16_t)); - - bool IsTeardownTraceEnabled() { - static const bool enabled = [] { - wchar_t value[32] = {}; - const DWORD len = GetEnvironmentVariableW(L"INFINIFRAME_TRACE_TEARDOWN", value, _countof(value)); - if (len == 0 || len >= _countof(value)) - return false; - - return _wcsicmp(value, L"1") == 0 - || _wcsicmp(value, L"true") == 0 - || _wcsicmp(value, L"yes") == 0 - || _wcsicmp(value, L"on") == 0; - }(); - - return enabled; - } - - void TraceTeardown(const wchar_t* format, ...) { - if (!IsTeardownTraceEnabled()) - return; - - wchar_t message[1024] = {}; - va_list args; - va_start(args, format); - _vsnwprintf_s(message, _countof(message), _TRUNCATE, format, args); - va_end(args); - - const std::wstring line = std::format( - L"[InfiniFrame][teardown][tid={}] {}\n", - GetCurrentThreadId(), - message - ); - OutputDebugStringW(line.c_str()); - std::fwprintf(stderr, L"%ls", line.c_str()); - std::fflush(stderr); - } - - std::wstring Utf8ToWide(const AutoString source) { - if (source == nullptr) - return {}; - - const auto* utf8 = reinterpret_cast(source); - const size_t utf8Length = strlen(utf8); - if (utf8Length == 0) - return {}; - - if (const auto validation = simdutf::validate_utf8_with_errors(utf8, utf8Length); validation.is_err()) - return {}; - - std::u16string utf16(simdutf::utf16_length_from_utf8(utf8, utf8Length), u'\0'); - const size_t written = simdutf::convert_valid_utf8_to_utf16( - utf8, - utf8Length, - reinterpret_cast(utf16.data()) - ); - utf16.resize(written); - - return { - reinterpret_cast(utf16.data()), - utf16.size() - }; - } - - std::string WideToUtf8(const AutoString 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; - } - - bool EnsureDirectoryWritable(const std::wstring& directoryPath) { - if (directoryPath.empty()) - return false; - - std::error_code createError; - std::filesystem::create_directories(directoryPath, createError); - if (createError) - return false; - - const std::wstring probePath = std::format( - L"{}\\{}.tmp", - directoryPath, - std::format(L".infiniframe-wv2-write-check-{}-{}-{}", GetCurrentProcessId(), GetCurrentThreadId(), GetTickCount64()) - ); - - HANDLE probeHandle = CreateFileW( - probePath.c_str(), - GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, - CREATE_ALWAYS, - FILE_ATTRIBUTE_TEMPORARY, - nullptr - ); - - if (probeHandle == INVALID_HANDLE_VALUE) - return false; - - CloseHandle(probeHandle); - DeleteFileW(probePath.c_str()); - return true; - } - - InfiniFrameWindow* LookupWindowInstance(const HWND hwnd) { - return reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); - } - - HWND ResolveParentWindowHandle(InfiniFrameWindow* parent) { - if (parent == nullptr) - return nullptr; - - HWND parentHwnd = parent->getHwnd(); - if (parentHwnd == nullptr || !IsWindow(parentHwnd)) - return nullptr; - - return parentHwnd; - } - - template - void ApplyPendingOwnerWindow(TImpl* impl, const wchar_t* phase) { - if (impl == nullptr) - return; - if (impl->_ownerAssigned) - return; - - if (impl->_pendingOwnerHwnd == nullptr || impl->_hWnd == nullptr) - return; - - if (impl->_pendingOwnerHwnd == impl->_hWnd) - return; - - if (!IsWindow(impl->_pendingOwnerHwnd) || !IsWindow(impl->_hWnd)) - return; - - SetLastError(0); - const LONG_PTR previousOwner = SetWindowLongPtr( - impl->_hWnd, - GWLP_HWNDPARENT, - reinterpret_cast(impl->_pendingOwnerHwnd) - ); - const DWORD lastError = GetLastError(); - - if (previousOwner == 0 && lastError != 0) { - TraceTeardown( - L"ApplyPendingOwnerWindow failed phase=%ls child=%p owner=%p err=%lu", - phase, - impl->_hWnd, - impl->_pendingOwnerHwnd, - lastError - ); - return; - } - - impl->_ownerAssigned = true; - - const DWORD childThreadId = GetWindowThreadProcessId(impl->_hWnd, nullptr); - const DWORD ownerThreadId = GetWindowThreadProcessId(impl->_pendingOwnerHwnd, nullptr); - TraceTeardown( - L"ApplyPendingOwnerWindow success phase=%ls child=%p owner=%p childTid=%lu ownerTid=%lu prev=%p", - phase, - impl->_hWnd, - impl->_pendingOwnerHwnd, - childThreadId, - ownerThreadId, - reinterpret_cast(previousOwner) - ); - } -} - - -struct InvokeWaitInfo { - std::mutex mutex; - std::condition_variable completionNotifier; - bool isCompleted = false; - bool isAbandoned = false; -}; - -struct ShowMessageParams { - std::wstring title; - std::wstring body; - 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(); - - _hInstance.store(hInstance, std::memory_order_release); - - // Register the window class - WNDCLASSEX wcx; - wcx.cbSize = sizeof(WNDCLASSEX); - wcx.style = CS_HREDRAW | CS_VREDRAW; - wcx.lpfnWndProc = WindowProc; - wcx.cbClsExtra = 0; - wcx.cbWndExtra = 0; - wcx.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(); - wcx.lpszMenuName = nullptr; - wcx.lpszClassName = CLASS_NAME; - wcx.hIconSm = LoadIcon(hInstance, IDI_APPLICATION); - - RegisterClassEx(&wcx); - - SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); -} - -InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { - 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) { - 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); - - if (initParams->StartString != nullptr) - m_impl->_startString = ToUTF16String(initParams->StartString); - - if (initParams->TemporaryFilesPath != nullptr) - m_impl->_temporaryFilesPath = ToUTF16String(initParams->TemporaryFilesPath); - - if (initParams->UserAgent != nullptr) - m_impl->_userAgent = ToUTF16String(initParams->UserAgent); - - if (initParams->BrowserControlInitParameters != nullptr) - m_impl->_browserControlInitParameters = ToUTF16String(initParams->BrowserControlInitParameters); - - if (initParams->NotificationRegistrationId != nullptr) - m_impl->_notificationRegistrationId = ToUTF16String(initParams->NotificationRegistrationId); - - - m_impl->_transparentEnabled = initParams->Transparent; - m_impl->_contextMenuEnabled = initParams->ContextMenuEnabled; - m_impl->_zoomEnabled = initParams->ZoomEnabled; - m_impl->_devToolsEnabled = initParams->DevToolsEnabled; - m_impl->_grantBrowserPermissions = initParams->GrantBrowserPermissions; - m_impl->_mediaAutoplayEnabled = initParams->MediaAutoplayEnabled; - m_impl->_fileSystemAccessEnabled = initParams->FileSystemAccessEnabled; - m_impl->_webSecurityEnabled = initParams->WebSecurityEnabled; - m_impl->_javascriptClipboardAccessEnabled = initParams->JavascriptClipboardAccessEnabled; - m_impl->_mediaStreamEnabled = initParams->MediaStreamEnabled; - m_impl->_smoothScrollingEnabled = initParams->SmoothScrollingEnabled; - m_impl->_ignoreCertificateErrorsEnabled = initParams->IgnoreCertificateErrorsEnabled; - m_impl->_notificationsEnabled = initParams->NotificationsEnabled; - - m_impl->_zoom = initParams->Zoom; - m_impl->_minWidth = initParams->MinWidth; - m_impl->_minHeight = initParams->MinHeight; - m_impl->_maxWidth = initParams->MaxWidth; - m_impl->_maxHeight = initParams->MaxHeight; - - //these handlers are ALWAYS hooked up - m_impl->_webMessageReceivedCallback = initParams->WebMessageReceivedHandler; - m_impl->_resizedCallback = initParams->ResizedHandler; - m_impl->_maximizedCallback = initParams->MaximizedHandler; - m_impl->_restoredCallback = initParams->RestoredHandler; - m_impl->_minimizedCallback = initParams->MinimizedHandler; - m_impl->_movedCallback = initParams->MovedHandler; - m_impl->_closingCallback = initParams->ClosingHandler; - m_impl->_closedCallback = initParams->ClosedHandler; - m_impl->_focusInCallback = initParams->FocusInHandler; - m_impl->_focusOutCallback = initParams->FocusOutHandler; - m_impl->_customSchemeCallback = initParams->CustomSchemeHandler; - - //copy strings from the fixed size array passed, but only if they have a value. - for (int i = 0; i < 16; ++i) { - if (initParams->CustomSchemeNames[i] != nullptr) - m_impl->_customSchemeNames.emplace_back(ToUTF16String(initParams->CustomSchemeNames[i])); - } - - m_impl->_parent = initParams->ParentInstance; - - int normalizedWidth = initParams->Width; - int normalizedHeight = initParams->Height; - int normalizedLeft = initParams->Left; - int normalizedTop = initParams->Top; - bool centerOnInitialize = initParams->CenterOnInitialize; - - if (initParams->UseOsDefaultSize) { - normalizedWidth = CW_USEDEFAULT; - normalizedHeight = CW_USEDEFAULT; - } - else { - if (normalizedWidth < 0) - normalizedWidth = CW_USEDEFAULT; - if (normalizedHeight < 0) - normalizedHeight = CW_USEDEFAULT; - } - - if (initParams->UseOsDefaultLocation) { - normalizedLeft = CW_USEDEFAULT; - normalizedTop = CW_USEDEFAULT; - } - - if (initParams->FullScreen) { - normalizedLeft = 0; - normalizedTop = 0; - normalizedWidth = GetSystemMetrics(SM_CXSCREEN); - normalizedHeight = GetSystemMetrics(SM_CYSCREEN); - } - - if (initParams->Chromeless) { - if (normalizedLeft == CW_USEDEFAULT && normalizedTop == CW_USEDEFAULT) - centerOnInitialize = true; - if (normalizedLeft == CW_USEDEFAULT) - normalizedLeft = 0; - if (normalizedTop == CW_USEDEFAULT) - normalizedTop = 0; - if (normalizedHeight == CW_USEDEFAULT) - normalizedHeight = 600; - if (normalizedWidth == CW_USEDEFAULT) - normalizedWidth = 800; - } - - if (normalizedHeight > initParams->MaxHeight) - normalizedHeight = initParams->MaxHeight; - if (normalizedHeight < initParams->MinHeight && initParams->MinHeight > 0) - normalizedHeight = initParams->MinHeight; - if (normalizedWidth > initParams->MaxWidth) - normalizedWidth = initParams->MaxWidth; - if (normalizedWidth < initParams->MinWidth && initParams->MinWidth > 0) - normalizedWidth = initParams->MinWidth; - - - const HWND parentWindowHandle = ResolveParentWindowHandle(m_impl->_parent); - m_impl->_pendingOwnerHwnd = parentWindowHandle; - - //Create the window - const HINSTANCE windowInstance = _hInstance.load(std::memory_order_acquire); - m_impl->_hWnd = CreateWindowEx( - initParams->Transparent ? WS_EX_LAYERED : 0, //WS_EX_OVERLAPPEDWINDOW, //An optional extended window style. - CLASS_NAME, //Window class - m_impl->_windowTitle.c_str(), //Window text - initParams->Chromeless || initParams->FullScreen ? WS_POPUP : WS_OVERLAPPEDWINDOW, //Window style - - // Size and position - normalizedLeft, normalizedTop, normalizedWidth, normalizedHeight, - - nullptr, //Parent window handle is set after creation via GWLP_HWNDPARENT to avoid cross-thread create-time interactions. - nullptr, //Menu - windowInstance, //Instance handle - this //Additional application data - ); - - ApplyPendingOwnerWindow(m_impl.get(), L"ctor"); - - if (initParams->WindowIconFile != nullptr) { - SetIconFile(initParams->WindowIconFile); - } - - - if (centerOnInitialize) - Center(); - - if (initParams->Minimized) - SetMinimized(true); - - if (initParams->Maximized) - SetMaximized(true); - - SetResizable(initParams->Resizable); - - 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->_dialog = std::make_unique(this); - - bool isAlreadyShown = initParams->Minimized || initParams->Maximized; - Show(isAlreadyShown); -} - -InfiniFrameWindow::~InfiniFrameWindow() { -} - -HWND InfiniFrameWindow::getHwnd() { - return m_impl->_hWnd; -} - - -LRESULT CALLBACK WindowProc(const HWND hwnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) { - switch (uMsg) { - case WM_NCCREATE: { - const auto* createParams = reinterpret_cast(lParam); - auto* instance = reinterpret_cast(createParams->lpCreateParams); - SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(instance)); - return TRUE; - } - 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 = LookupWindowInstance(hwnd); - if (instance) { - if (LOWORD(wParam) == WA_INACTIVE) { - instance->InvokeFocusOut(); - } - else { - instance->FocusWebView2(); - instance->InvokeFocusIn(); - - return 0; - } - } - break; - } - case WM_CLOSE: { - InfiniFrameWindow * instance = LookupWindowInstance(hwnd); - if (instance) { - TraceTeardown(L"WM_CLOSE hwnd=%p instance=%p", hwnd, instance); - bool doNotClose = instance->InvokeClose(); - - if (!doNotClose) { - // On Windows ARM64 we observed occasional access violations during teardown when - // owner/owned windows live on different UI threads. Detach owner linkage before - // destruction to avoid cross-thread owner-chain teardown races. - SetLastError(0); - const LONG_PTR previousOwner = SetWindowLongPtr(hwnd, GWLP_HWNDPARENT, 0); - const DWORD ownerDetachError = GetLastError(); - if (previousOwner != 0 || ownerDetachError == 0) { - TraceTeardown( - L"WM_CLOSE detached owner hwnd=%p prevOwner=%p err=%lu", - hwnd, - reinterpret_cast(previousOwner), - ownerDetachError - ); - } - - DestroyWindow(hwnd); - } - } - - return 0; - } - case WM_DESTROY: { - InfiniFrameWindow * instance = LookupWindowInstance(hwnd); - if (instance) { - instance->m_impl->_isClosingOrClosed.store(true, std::memory_order_release); - TraceTeardown(L"WM_DESTROY begin hwnd=%p instance=%p", hwnd, instance); - instance->CloseWebView(); - instance->InvokeClosed(); - TraceTeardown(L"WM_DESTROY end hwnd=%p instance=%p", hwnd, instance); - } - // Terminate the message loop of the thread that owns this window - if (hwnd == messageLoopRootWindowHandle) - PostQuitMessage(0); - - return 0; - } - case WM_NCDESTROY: { - InfiniFrameWindow * instance = LookupWindowInstance(hwnd); - if (instance) { - instance->m_impl->_isClosingOrClosed.store(true, std::memory_order_release); - instance->m_impl->_hWnd = nullptr; - } - TraceTeardown(L"WM_NCDESTROY hwnd=%p instance=%p", hwnd, instance); - SetWindowLongPtr(hwnd, GWLP_USERDATA, 0); - break; - } - case WM_USER_INVOKE: { - auto callback = reinterpret_cast(wParam); - auto* waitInfo = reinterpret_cast(lParam); - - if (waitInfo == nullptr) { - if (callback) - callback(); - return 0; - } - - bool deleteWaitInfo = false; - { - std::lock_guard guard(waitInfo->mutex); - if (!waitInfo->isAbandoned && callback) - callback(); - waitInfo->isCompleted = true; - deleteWaitInfo = waitInfo->isAbandoned; - } - - waitInfo->completionNotifier.notify_one(); - - if (deleteWaitInfo) - delete waitInfo; - return 0; - } - case WM_GETMINMAXINFO: { - InfiniFrameWindow * instance = LookupWindowInstance(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 = LookupWindowInstance(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 = LookupWindowInstance(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->_isClosingOrClosed.store(true, std::memory_order_release); - const bool deferEnvironmentRelease = - m_impl->_isWebView2Initializing && m_impl->_webviewController == nullptr; - TraceTeardown( - L"CloseWebView begin instance=%p hwnd=%p controller=%p webview=%p env=%p", - this, - m_impl->_hWnd, - m_impl->_webviewController.get(), - m_impl->_webviewWindow.get(), - m_impl->_webviewEnvironment.get() - ); - - // Keep teardown non-blocking in WM_DESTROY path: Close() the controller first and - // avoid synchronous event unsubscription / Stop() calls that can stall shutdown. - if (m_impl->_webviewController != nullptr) { - m_impl->_webviewController->Close(); - m_impl->_webviewController = nullptr; - } - - m_impl->_webviewWindow = nullptr; - - m_impl->_hasWebMessageReceivedToken = false; - m_impl->_hasWebResourceRequestedToken = false; - m_impl->_hasPermissionRequestedToken = false; - m_impl->_webMessageReceivedToken = {}; - m_impl->_webResourceRequestedTokenForCustomScheme = {}; - m_impl->_permissionRequestedToken = {}; - - if (m_impl->_webviewEnvironment != nullptr && !deferEnvironmentRelease) { - m_impl->_webviewEnvironment = nullptr; - } - - m_impl->_isInitialized = false; - if (!deferEnvironmentRelease) - m_impl->_isWebView2Initializing = false; - - if (deferEnvironmentRelease) { - TraceTeardown( - L"CloseWebView deferring environment release instance=%p env=%p", - this, - m_impl->_webviewEnvironment.get() - ); - } - - TraceTeardown(L"CloseWebView end instance=%p", this); -} - - -void InfiniFrameWindow::Center() { - int screenDpi = GetDpiForWindow(m_impl->_hWnd); - int screenHeight = GetSystemMetricsForDpi(SM_CYSCREEN, screenDpi); - int screenWidth = GetSystemMetricsForDpi(SM_CXSCREEN, screenDpi); - - RECT windowRect = {}; - GetWindowRect(m_impl->_hWnd, &windowRect); - int windowHeight = windowRect.bottom - windowRect.top; - int windowWidth = windowRect.right - windowRect.left; - - int left = (screenWidth / 2) - (windowWidth / 2); - int top = (screenHeight / 2) - (windowHeight / 2); - - SetPosition(left, top); -} - -void InfiniFrameWindow::Close() { - PostMessage(m_impl->_hWnd, WM_CLOSE, 0, 0); -} - -void InfiniFrameWindow::GetTransparentEnabled(bool* enabled) const { - if (!m_impl->_webviewController) { - *enabled = m_impl->_transparentEnabled; - return; - } - wil::com_ptr controller2; - if (FAILED(m_impl->_webviewController->QueryInterface(&controller2)) || !controller2) { - *enabled = m_impl->_transparentEnabled; - return; - } - COREWEBVIEW2_COLOR backgroundColor; - controller2->get_DefaultBackgroundColor(&backgroundColor); - *enabled = backgroundColor.A == 0; -} - -void InfiniFrameWindow::GetContextMenuEnabled(bool* enabled) const { - if (!m_impl->_webviewWindow) { - *enabled = m_impl->_contextMenuEnabled; - return; - } - wil::com_ptr settings; - if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { - BOOL boolValue = FALSE; - settings->get_AreDefaultContextMenusEnabled(&boolValue); - *enabled = (boolValue != FALSE); - } -} - -void InfiniFrameWindow::GetZoomEnabled(bool* enabled) const { - if (!m_impl->_webviewWindow) { - *enabled = m_impl->_zoomEnabled; - return; - } - wil::com_ptr settings; - if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { - BOOL boolValue = FALSE; - settings->get_IsZoomControlEnabled(&boolValue); - *enabled = (boolValue != FALSE); - } -} - -void InfiniFrameWindow::GetDevToolsEnabled(bool* enabled) const { - if (!m_impl->_webviewWindow) { - *enabled = m_impl->_devToolsEnabled; - return; - } - wil::com_ptr settings; - if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { - BOOL boolValue = FALSE; - settings->get_AreDevToolsEnabled(&boolValue); - *enabled = (boolValue != FALSE); - } -} - -void InfiniFrameWindow::GetFullScreen(bool* fullScreen) const { - LONG lStyles = GetWindowLong(m_impl->_hWnd, GWL_STYLE); - *fullScreen = (lStyles & WS_POPUP) != 0; -} - -void InfiniFrameWindow::GetGrantBrowserPermissions(bool* grant) const { - *grant = m_impl->_grantBrowserPermissions; -} - -AutoString InfiniFrameWindow::GetUserAgent() const { - return AllocateStringCopy(m_impl->_userAgent); -} - -void InfiniFrameWindow::GetMediaAutoplayEnabled(bool* enabled) const { - *enabled = m_impl->_mediaAutoplayEnabled; -} - -void InfiniFrameWindow::GetFileSystemAccessEnabled(bool* enabled) const { - *enabled = m_impl->_fileSystemAccessEnabled; -} - -void InfiniFrameWindow::GetWebSecurityEnabled(bool* enabled) const { - *enabled = m_impl->_webSecurityEnabled; -} - -void InfiniFrameWindow::GetJavascriptClipboardAccessEnabled(bool* enabled) const { - *enabled = m_impl->_javascriptClipboardAccessEnabled; -} - -void InfiniFrameWindow::GetMediaStreamEnabled(bool* enabled) const { - *enabled = m_impl->_mediaStreamEnabled; -} - -void InfiniFrameWindow::GetSmoothScrollingEnabled(bool* enabled) const { - *enabled = m_impl->_smoothScrollingEnabled; -} - -void InfiniFrameWindow::GetIgnoreCertificateErrorsEnabled(bool* enabled) const { - *enabled = m_impl->_ignoreCertificateErrorsEnabled; -} - -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); -} - -void InfiniFrameWindow::GetMaximized(bool* isMaximized) const { - LONG lStyles = GetWindowLong(m_impl->_hWnd, GWL_STYLE); - *isMaximized = (lStyles & WS_MAXIMIZE) != 0; -} - -void InfiniFrameWindow::GetMinimized(bool* isMinimized) const { - LONG lStyles = GetWindowLong(m_impl->_hWnd, GWL_STYLE); - *isMinimized = (lStyles & WS_MINIMIZE) != 0; -} - -void InfiniFrameWindow::GetPosition(int* x, int* y) const { - RECT rect = {}; - GetWindowRect(m_impl->_hWnd, &rect); - if (x) - *x = rect.left; - if (y) - *y = rect.top; -} - -void InfiniFrameWindow::GetResizable(bool* resizable) const { - LONG lStyles = GetWindowLong(m_impl->_hWnd, GWL_STYLE); - *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); - if (width) - *width = rect.right - rect.left; - if (height) - *height = rect.bottom - rect.top; -} - -void InfiniFrameWindow::GetMaxSize(int* width, int* height) const { - if (width) - *width = m_impl->_maxWidth; - if (height) - *height = m_impl->_maxHeight; -} - -void InfiniFrameWindow::GetMinSize(int* width, int* height) const { - if (width) - *width = m_impl->_minWidth; - if (height) - *height = m_impl->_minHeight; -} - -AutoString InfiniFrameWindow::GetTitle() const { - return AllocateStringCopy(m_impl->_windowTitle); -} - -void InfiniFrameWindow::GetTopmost(bool* topmost) const { - // Return the stored intent rather than the live HWND style - *topmost = m_impl->_topmost; -} - -void InfiniFrameWindow::GetZoom(int* zoom) const { - if (zoom == nullptr) - return; - if (m_impl->_webviewController == nullptr) { - *zoom = m_impl->_zoom; - return; - } - - double rawValue = 0; - if (FAILED(m_impl->_webviewController->get_ZoomFactor(&rawValue))) { - *zoom = m_impl->_zoom; - return; - } - - rawValue = (rawValue * 100.0) + 0.5; //account for rounding issues - *zoom = static_cast(rawValue); -} - - -void InfiniFrameWindow::NavigateToString(AutoString content) { - std::wstring wideContent = ToUTF16String(content); - m_impl->_webviewWindow->NavigateToString(wideContent.c_str()); -} - -void InfiniFrameWindow::NavigateToUrl(AutoString url) { - std::wstring wideUrl = ToUTF16String(url); - m_impl->_webviewWindow->Navigate(wideUrl.c_str()); -} - -void InfiniFrameWindow::Restore() { - ShowWindow(m_impl->_hWnd, SW_RESTORE); -} - -void InfiniFrameWindow::SendWebMessage(AutoString message) { - if (!m_impl->_webviewWindow || !m_impl->_webviewController || !m_impl->_hWnd || !IsWindow(m_impl->_hWnd)) - return; - - std::wstring wideMessage = ToUTF16String(message); - m_impl->_webviewWindow->PostWebMessageAsString(wideMessage.c_str()); -} - - -void InfiniFrameWindow::SetTransparentEnabled(const bool enabled) { - m_impl->_transparentEnabled = enabled; - if (!m_impl->_webviewController || !m_impl->_webviewWindow) - return; - wil::com_ptr controller2; - if (FAILED(m_impl->_webviewController->QueryInterface(&controller2)) || !controller2) - return; - COREWEBVIEW2_COLOR backgroundColor; - controller2->get_DefaultBackgroundColor(&backgroundColor); - backgroundColor.A = enabled ? 0 : 255; - controller2->put_DefaultBackgroundColor(backgroundColor); - m_impl->_webviewWindow->Reload(); -} - -void InfiniFrameWindow::SetContextMenuEnabled(const bool enabled) { - m_impl->_contextMenuEnabled = enabled; - if (!m_impl->_webviewWindow) - return; - wil::com_ptr settings; - if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { - settings->put_AreDefaultContextMenusEnabled(enabled); - m_impl->_webviewWindow->Reload(); - } -} - -void InfiniFrameWindow::SetZoomEnabled(const bool enabled) { - m_impl->_zoomEnabled = enabled; - if (!m_impl->_webviewWindow) - return; - wil::com_ptr settings; - if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { - settings->put_IsZoomControlEnabled(enabled); - m_impl->_webviewWindow->Reload(); - } -} - -void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { - m_impl->_devToolsEnabled = enabled; - if (!m_impl->_webviewWindow) - return; - wil::com_ptr settings; - if (SUCCEEDED(m_impl->_webviewWindow->get_Settings(&settings)) && settings) { - settings->put_AreDevToolsEnabled(enabled); - m_impl->_webviewWindow->Reload(); - } -} - -void InfiniFrameWindow::SetFullScreen(const bool fullScreen) { - LONG_PTR style = GetWindowLongPtr(m_impl->_hWnd, GWL_STYLE); - if (fullScreen) { - GetWindowRect(m_impl->_hWnd, &m_impl->_savedRect); - m_impl->_hasSavedRect = true; - - style |= WS_POPUP; - style &= (~WS_OVERLAPPEDWINDOW); - SetWindowLongPtr(m_impl->_hWnd, GWL_STYLE, style); - - HMONITOR monitor = MonitorFromWindow(m_impl->_hWnd, MONITOR_DEFAULTTONEAREST); - MONITORINFO monitorInfo = {sizeof(monitorInfo)}; - - if (GetMonitorInfoW(monitor, &monitorInfo)) { - RECT rc = monitorInfo.rcMonitor; - SetWindowPos( - m_impl->_hWnd, HWND_TOP, - rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, - SWP_FRAMECHANGED | SWP_NOOWNERZORDER - ); - } - else { - SetWindowPos( - m_impl->_hWnd, HWND_TOP, - 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), - SWP_FRAMECHANGED | SWP_NOOWNERZORDER - ); - } - } - else { - style |= WS_OVERLAPPEDWINDOW; - style &= (~WS_POPUP); - SetWindowLongPtr(m_impl->_hWnd, GWL_STYLE, style); - - if (m_impl->_hasSavedRect) { - RECT& r = m_impl->_savedRect; - SetWindowPos( - m_impl->_hWnd, HWND_TOP, - r.left, r.top, r.right - r.left, r.bottom - r.top, - SWP_FRAMECHANGED | SWP_NOOWNERZORDER - ); - m_impl->_hasSavedRect = false; - } - } -} - -void InfiniFrameWindow::SetIconFile(const AutoString filename) { - std::wstring wideFilename = ToUTF16String(filename); - m_impl->_iconFileName = wideFilename; - if (wideFilename.empty()) - return; - - HICON iconSmall = static_cast(LoadImageW( - nullptr, wideFilename.c_str(), - IMAGE_ICON, 16, 16, - LR_LOADFROMFILE | LR_LOADTRANSPARENT | LR_SHARED - )); - HICON iconBig = static_cast(LoadImageW( - nullptr, wideFilename.c_str(), - IMAGE_ICON, 32, 32, - LR_LOADFROMFILE | LR_LOADTRANSPARENT | LR_SHARED - )); - - if (iconSmall && iconBig) { - SendMessageW(m_impl->_hWnd, WM_SETICON, ICON_SMALL, reinterpret_cast(iconSmall)); - SendMessageW(m_impl->_hWnd, WM_SETICON, ICON_BIG, reinterpret_cast(iconBig)); - } -} - -void InfiniFrameWindow::SetMinimized(const bool minimized) { - if (minimized) - ShowWindow(m_impl->_hWnd, SW_MINIMIZE); - else - ShowWindow(m_impl->_hWnd, SW_NORMAL); -} - -void InfiniFrameWindow::SetMinSize(const int width, const int height) { - m_impl->_minWidth = width; - m_impl->_minHeight = height; - - int currWidth, currHeight; - GetSize(&currWidth, &currHeight); - if (currWidth < m_impl->_minWidth) - SetSize(m_impl->_minWidth, currHeight); - if (currHeight < m_impl->_minHeight) - SetSize(currWidth, m_impl->_minHeight); -} - -void InfiniFrameWindow::SetMaximized(const bool maximized) { - if (maximized) - ShowWindow(m_impl->_hWnd, SW_MAXIMIZE); - else - ShowWindow(m_impl->_hWnd, SW_NORMAL); -} - -void InfiniFrameWindow::SetMaxSize(const int width, const int height) { - m_impl->_maxWidth = width; - m_impl->_maxHeight = height; - - int currWidth, currHeight; - GetSize(&currWidth, &currHeight); - if (currWidth > m_impl->_maxWidth) - SetSize(m_impl->_maxWidth, currHeight); - if (currHeight > m_impl->_maxHeight) - SetSize(currWidth, m_impl->_maxHeight); -} - -void InfiniFrameWindow::SetPosition(const int x, const int y) { - SetWindowPos(m_impl->_hWnd, HWND_TOP, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER); -} - -void InfiniFrameWindow::SetResizable(const bool resizable) { - LONG_PTR style = GetWindowLongPtr(m_impl->_hWnd, GWL_STYLE); - if (resizable) - style |= WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; - else - style &= (~WS_THICKFRAME) & (~WS_MINIMIZEBOX) & (~WS_MAXIMIZEBOX); - SetWindowLongPtr(m_impl->_hWnd, GWL_STYLE, style); -} - -void InfiniFrameWindow::SetSize(const int width, const int height) { - SetWindowPos(m_impl->_hWnd, HWND_TOP, 0, 0, width, height, SWP_NOMOVE | SWP_NOZORDER); -} - -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()); - } -} - -void InfiniFrameWindow::SetTopmost(const bool topmost) { - m_impl->_topmost = topmost; - LONG_PTR style = GetWindowLongPtr(m_impl->_hWnd, GWL_EXSTYLE); - if (topmost) - style |= WS_EX_TOPMOST; - else - style &= (~WS_EX_TOPMOST); - SetWindowLongPtr(m_impl->_hWnd, GWL_EXSTYLE, style); - SetWindowPos(m_impl->_hWnd, topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); -} - -void InfiniFrameWindow::SetZoom(const int zoom) { - if (zoom < 25 || zoom > 500) - return; - - m_impl->_zoom = zoom; - if (m_impl->_webviewController == nullptr) - return; - - const double newZoom = zoom / 100.0; - m_impl->_webviewController->put_ZoomFactor(newZoom); -} - -void InfiniFrameWindow::SetFocused() { - if (!m_impl->_hWnd) - return; - - // If minimized, restore first - if (IsIconic(m_impl->_hWnd)) - ShowWindow(m_impl->_hWnd, SW_RESTORE); - - // Try to request foreground rights - AllowSetForegroundWindow(ASFW_ANY); - - // Bring the window to the top and set focus/activation - HWND hwndForeground = GetForegroundWindow(); - const DWORD fgThread = hwndForeground ? GetWindowThreadProcessId(hwndForeground, nullptr) : 0; - const DWORD thisThread = GetCurrentThreadId(); - - // Temporarily attach thread inputs to improve the chances of success - if (fgThread && fgThread != thisThread) - AttachThreadInput(fgThread, thisThread, TRUE); - - ShowWindow(m_impl->_hWnd, SW_SHOW); - SetForegroundWindow(m_impl->_hWnd); - BringWindowToTop(m_impl->_hWnd); - SetActiveWindow(m_impl->_hWnd); - SetFocus(m_impl->_hWnd); - - if (fgThread && fgThread != thisThread) - AttachThreadInput(fgThread, thisThread, FALSE); - - // Also move focus to the embedded WebView2, if available - 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() { - ApplyPendingOwnerWindow(m_impl.get(), L"wait_for_exit"); - - messageLoopRootWindowHandle = m_impl->_hWnd; - TraceTeardown(L"WaitForExit start instance=%p hwnd=%p", this, m_impl->_hWnd); - - // Run the message loop - MSG msg = {}; - while (true) { - const int getMessageResult = GetMessage(&msg, nullptr, 0, 0); - if (getMessageResult == -1) { - TraceTeardown(L"WaitForExit GetMessage failed err=%lu", GetLastError()); - break; - } - if (getMessageResult == 0) - break; - - TranslateMessage(&msg); - DispatchMessage(&msg); - } - - messageLoopRootWindowHandle = nullptr; - TraceTeardown(L"WaitForExit end instance=%p hwnd=%p", this, m_impl->_hWnd); -} - - -//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; - - auto* waitInfo = new InvokeWaitInfo(); - if (!PostMessage( - m_impl->_hWnd, WM_USER_INVOKE, reinterpret_cast(callback), reinterpret_cast(waitInfo) - )) { - delete waitInfo; - return; - } - - std::unique_lock uLock(waitInfo->mutex); - const bool completed = waitInfo->completionNotifier.wait_for( - uLock, - std::chrono::seconds(15), - [&] { - return waitInfo->isCompleted; - } - ); - - if (!completed) { - bool deleteWaitInfo = false; - if (waitInfo->isCompleted) - deleteWaitInfo = true; - else - waitInfo->isAbandoned = true; - - uLock.unlock(); - - if (deleteWaitInfo) - delete waitInfo; - - OutputDebugStringW(L"InfiniFrameWindow::Invoke timed out waiting for UI thread callback.\n"); - return; - } - - uLock.unlock(); - delete waitInfo; -} - -std::string InfiniFrameWindow::ToUTF8String(const AutoString source) const { - return WideToUtf8(source); -} - -std::wstring InfiniFrameWindow::ToUTF16String(const AutoString source) const { - return Utf8ToWide(source); -} - -void InfiniFrameWindow::AttachWebView() { - if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) - return; - - if (m_impl->_isWebView2Initializing || m_impl->_isInitialized) - return; - m_impl->_isWebView2Initializing = true; - - std::wstring configuredRuntimePath; - { - std::lock_guard lock(webview2RuntimePathMutex); - configuredRuntimePath = _webview2RuntimePath; - } - PCWSTR runtimePath = configuredRuntimePath.empty() ? nullptr : configuredRuntimePath.c_str(); - - 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 - ); - m_impl->_isWebView2Initializing = false; - return; - } - - PCWSTR userDataPath = nullptr; - if (!m_impl->_temporaryFilesPath.empty()) { - if (EnsureDirectoryWritable(m_impl->_temporaryFilesPath)) - userDataPath = m_impl->_temporaryFilesPath.c_str(); - else - TraceTeardown( - L"AttachWebView: temporary user-data path is not writable. Falling back to default path. path=%ls", - m_impl->_temporaryFilesPath.c_str() - ); - } - - HRESULT envResult = CreateCoreWebView2EnvironmentWithOptions( - runtimePath, - userDataPath, - options.Get(), - Callback< - ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>( - [this]( - const HRESULT result, - ICoreWebView2Environment* env - ) -> HRESULT { - if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) { - m_impl->_isWebView2Initializing = false; - m_impl->_webviewEnvironment = nullptr; - TraceTeardown(L"CreateEnvironment callback while closing; ignoring"); - return S_OK; - } - if (result != S_OK) { - m_impl->_isWebView2Initializing = false; - TraceTeardown(L"CreateEnvironment callback failed hr=0x%08X", static_cast(result)); - return result; - } - if (env == nullptr) { - m_impl->_isWebView2Initializing = false; - return E_POINTER; - } - HRESULT envResult = env->QueryInterface( - &m_impl->_webviewEnvironment - ); - if (envResult != S_OK) { - m_impl->_isWebView2Initializing = false; - return envResult; - } - - const HRESULT createControllerHr = env->CreateCoreWebView2Controller( - m_impl->_hWnd, - Callback< - ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>( - [this]( - const HRESULT result, - ICoreWebView2Controller* controller - ) -> - HRESULT { - if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) { - if (controller != nullptr) - controller->Close(); - m_impl->_webviewController = nullptr; - m_impl->_webviewWindow = nullptr; - m_impl->_webviewEnvironment = nullptr; - m_impl->_isWebView2Initializing = false; - TraceTeardown(L"CreateController callback while closing; ignoring"); - return S_OK; - } - if (result != S_OK) { - m_impl->_isWebView2Initializing = false; - TraceTeardown(L"CreateController callback failed hr=0x%08X", static_cast(result)); - return result; - } - if (controller == nullptr) { - m_impl->_isWebView2Initializing = false; - return E_POINTER; - } - - HRESULT envResult = controller-> - QueryInterface( - &m_impl-> - _webviewController - ); - if (envResult != S_OK) { - m_impl->_isWebView2Initializing = false; - return envResult; - } - m_impl->_webviewController->get_CoreWebView2(&m_impl->_webviewWindow); - if (!m_impl->_webviewWindow) { - m_impl->_isWebView2Initializing = false; - return E_FAIL; - } - - const auto js_wide = Embedded::InfiniFrameJsUtf16(); - 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}); - - 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>( - [this]( - ICoreWebView2*, - ICoreWebView2WebMessageReceivedEventArgs - * args - ) -> HRESULT { - if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) - 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') - && m_impl->_webviewWindow != nullptr - ) { - m_impl-> - _webviewWindow-> - get_Source( - &source - ); - } - m_impl-> - _webMessageReceivedCallback( - message. - get(), - source. - get() - ); - return S_OK; - } - ).Get(), - &webMessageToken - ); - m_impl->_webMessageReceivedToken = webMessageToken; - m_impl->_hasWebMessageReceivedToken = true; - - 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>( - [this]( - ICoreWebView2*, - ICoreWebView2WebResourceRequestedEventArgs - * args - ) { - if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) - return S_OK; - - 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 - ); - m_impl->_webResourceRequestedTokenForCustomScheme = webResourceRequestedToken; - m_impl->_hasWebResourceRequestedToken = true; - - EventRegistrationToken - permissionRequestedToken; - m_impl->_webviewWindow-> - add_PermissionRequested( - Callback< - ICoreWebView2PermissionRequestedEventHandler>( - [this]( - ICoreWebView2*, - ICoreWebView2PermissionRequestedEventArgs - * args - ) -> HRESULT { - if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) - return S_OK; - - if (m_impl-> - _grantBrowserPermissions) - args-> - put_State( - COREWEBVIEW2_PERMISSION_STATE_ALLOW - ); - return S_OK; - } - ) - .Get(), - &permissionRequestedToken - ); - m_impl->_permissionRequestedToken = permissionRequestedToken; - m_impl->_hasPermissionRequestedToken = true; - - 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); - - HRESULT addScriptHr = m_impl->_webviewWindow->AddScriptToExecuteOnDocumentCreated( - js_wide.c_str(), - Callback( - [nav, this](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()); - if (m_impl->_isClosingOrClosed.load(std::memory_order_acquire)) - return S_OK; - 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(); - - RefitContent(); - - FocusWebView2(); - - // Re-apply if topmost was requested - if (m_impl->_topmost) - SetTopmost(true); - - m_impl->_isInitialized = true; - m_impl->_isWebView2Initializing = false; - return S_OK; - } - ).Get() - ); - if (FAILED(createControllerHr)) - m_impl->_isWebView2Initializing = false; - - return createControllerHr; - } - ).Get() - ); - - if (envResult != S_OK) { - m_impl->_isWebView2Initializing = false; - _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); - if (versionInfo != nullptr) - CoTaskMemFree(versionInfo); - - if (ensureInstalledResult != S_OK) - return InstallWebView2(); - - return true; -} - -bool InfiniFrameWindow::InstallWebView2() { - auto srcURL = L"https://go.microsoft.com/fwlink/p/?LinkId=2124703"; - auto destFile = L"MicrosoftEdgeWebview2Setup.exe"; - - if (S_OK == URLDownloadToFile(nullptr, srcURL, destFile, 0, nullptr)) { - std::wstring command = L"MicrosoftEdgeWebview2Setup.exe"; - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&pi, sizeof(pi)); - - bool success = CreateProcess( - nullptr, // No module name (use command line) - command.data(), // Command line - nullptr, // Process handle not inheritable - nullptr, // Thread handle not inheritable - FALSE, // Set handle inheritance to FALSE - 0, // No creation flags - nullptr, // Use parent's environment block - nullptr, // Use parent's starting directory - &si, // Pointer to STARTUPINFO structure - &pi - ); // Pointer to PROCESS_INFORMATION structure - - if (success) { - // wait for the installation to complete - WaitForSingleObject(pi.hProcess, INFINITE); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - } - - return success; - } - - return false; -} - -void InfiniFrameWindow::RefitContent() { - if (m_impl->_webviewController) { - RECT bounds; - GetClientRect(m_impl->_hWnd, &bounds); - m_impl->_webviewController->put_Bounds(bounds); - } -} - -void InfiniFrameWindow::FocusWebView2() { - if (m_impl->_webviewController) { - m_impl->_webviewController->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); - } -} - -void InfiniFrameWindow::NotifyWebView2WindowMove() { - if (m_impl->_webviewController) { - m_impl->_webviewController->NotifyParentWindowPositionChanged(); - } -} - -void InfiniFrameWindow::ClearBrowserAutoFill() { - if (!m_impl->_webviewWindow) - return; - - auto webview15 = m_impl->_webviewWindow.try_query(); - if (webview15) { - wil::com_ptr profile; - webview15->get_Profile(&profile); - auto profile2 = profile.try_query(); - - if (profile2) { - COREWEBVIEW2_BROWSING_DATA_KINDS dataKinds = - (COREWEBVIEW2_BROWSING_DATA_KINDS) - ( - COREWEBVIEW2_BROWSING_DATA_KINDS_GENERAL_AUTOFILL | - COREWEBVIEW2_BROWSING_DATA_KINDS_PASSWORD_AUTOSAVE - ); - - profile2->ClearBrowsingData( - dataKinds, - Callback( - [this]( - HRESULT - ) - -> HRESULT { - return S_OK; - } - ) - .Get() - ); - } - } -} - -void InfiniFrameWindow::SetWebView2RuntimePath(const AutoString pathToWebView2) { - if (pathToWebView2 == nullptr) - return; - - std::wstring widePath = Utf8ToWide(pathToWebView2); - std::lock_guard lock(webview2RuntimePathMutex); - wcsncpy_s(_webview2RuntimePath, widePath.c_str(), _countof(_webview2RuntimePath)); -} - -void InfiniFrameWindow::Show(const bool isAlreadyShown) { - if (!isAlreadyShown) - ShowWindow(m_impl->_hWnd, SW_SHOWDEFAULT); - - UpdateWindow(m_impl->_hWnd); - - // WebView2 must be created after the window is visible. - if (!m_impl->_webviewController) { - bool hasConfiguredRuntimePath = false; - { - std::lock_guard lock(webview2RuntimePathMutex); - hasConfiguredRuntimePath = wcsnlen(_webview2RuntimePath, _countof(_webview2RuntimePath)) > 0; - } - if (hasConfiguredRuntimePath || EnsureWebViewIsInstalled()) - AttachWebView(); - else - exit(0); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Dialog and Scheme -// --------------------------------------------------------------------------------------------------------------------- - -InfiniFrameDialog* InfiniFrameWindow::GetDialog() const { - return m_impl->_dialog.get(); -} - -void InfiniFrameWindow::AddCustomSchemeName(const AutoStringConst scheme) { - if (scheme) - m_impl->_customSchemeNames.emplace_back(ToUTF16String(const_cast(scheme))); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Callback setters -// --------------------------------------------------------------------------------------------------------------------- - -void InfiniFrameWindow::SetClosingCallback(const ClosingCallback callback) { - m_impl->_closingCallback = callback; -} - -void InfiniFrameWindow::SetClosedCallback(const ClosedCallback callback) { - m_impl->_closedCallback = callback; -} - -void InfiniFrameWindow::SetFocusInCallback(const FocusInCallback callback) { - m_impl->_focusInCallback = callback; -} - -void InfiniFrameWindow::SetFocusOutCallback(const FocusOutCallback callback) { - m_impl->_focusOutCallback = callback; -} - -void InfiniFrameWindow::SetMovedCallback(const MovedCallback callback) { - m_impl->_movedCallback = callback; -} - -void InfiniFrameWindow::SetResizedCallback(const ResizedCallback callback) { - m_impl->_resizedCallback = callback; -} - -void InfiniFrameWindow::SetMaximizedCallback(const MaximizedCallback callback) { - m_impl->_maximizedCallback = callback; -} - -void InfiniFrameWindow::SetRestoredCallback(const RestoredCallback callback) { - m_impl->_restoredCallback = callback; -} - -void InfiniFrameWindow::SetMinimizedCallback(const MinimizedCallback callback) { - m_impl->_minimizedCallback = callback; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Invoke callbacks -// --------------------------------------------------------------------------------------------------------------------- - -bool InfiniFrameWindow::InvokeClose() const noexcept { - if (m_impl->_closingCallback) - return m_impl->_closingCallback(); - return false; -} - -void InfiniFrameWindow::InvokeClosed() const noexcept { - if (!m_impl->_closedCallback) return; - m_impl->_closedCallback(); -} - -void InfiniFrameWindow::InvokeFocusIn() const noexcept { - if (m_impl->_focusInCallback) - m_impl->_focusInCallback(); -} - -void InfiniFrameWindow::InvokeFocusOut() const noexcept { - if (m_impl->_focusOutCallback) - m_impl->_focusOutCallback(); -} - -void InfiniFrameWindow::InvokeMove(int x, int y) const noexcept { - if (m_impl->_movedCallback) - m_impl->_movedCallback(x, y); -} - -void InfiniFrameWindow::InvokeResize(int width, int height) const noexcept { - if (m_impl->_resizedCallback) - m_impl->_resizedCallback(width, height); -} - -void InfiniFrameWindow::InvokeMaximized() const noexcept { - if (m_impl->_maximizedCallback) - m_impl->_maximizedCallback(); -} - -void InfiniFrameWindow::InvokeRestored() const noexcept { - if (m_impl->_restoredCallback) - m_impl->_restoredCallback(); -} - -void InfiniFrameWindow::InvokeMinimized() const noexcept { - if (m_impl->_minimizedCallback) - m_impl->_minimizedCallback(); -} diff --git a/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Dialog.cpp b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Dialog.cpp new file mode 100644 index 000000000..31ba132fe --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Dialog.cpp @@ -0,0 +1,90 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/Exports/Exports.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern "C" { +EXPORTED InteropStatus InfiniFrame_ShowOpenFile( + InfiniFrameWindow* inst, + const AutoString title, + const AutoString defaultPath, + const bool multiSelect, + AutoString* filters, + const int filterCount, + int* resultCount, + AutoString** values +) { + ResetOut(resultCount, 0); + ResetOut(values, static_cast(nullptr)); + return RunWindowExportStatus(inst, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(resultCount, "resultCount")) + return; + if (!EnsureOutNotNull(values, "values")) + return; + if (filterCount < 0) + throw std::invalid_argument("Argument 'filterCount' must be >= 0."); + *values = window->GetDialog()->ShowOpenFile( + NullToEmpty(title), NullToEmpty(defaultPath), multiSelect, filters, filterCount, resultCount + ); + }); +} + +EXPORTED InteropStatus InfiniFrame_ShowOpenFolder( + InfiniFrameWindow* inst, + const AutoString title, + const AutoString defaultPath, + const bool multiSelect, + int* resultCount, + AutoString** values +) { + ResetOut(resultCount, 0); + ResetOut(values, static_cast(nullptr)); + return RunWindowExportStatus(inst, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(resultCount, "resultCount")) + return; + if (!EnsureOutNotNull(values, "values")) + return; + *values = + window->GetDialog()->ShowOpenFolder(NullToEmpty(title), NullToEmpty(defaultPath), multiSelect, resultCount); + }); +} + +EXPORTED InteropStatus InfiniFrame_ShowSaveFile( + InfiniFrameWindow* inst, + const AutoString title, + const AutoString defaultPath, + AutoString* filters, + const int filterCount, + const AutoString defaultFileName, + AutoString* value +) { + ResetOut(value, static_cast(nullptr)); + return RunWindowExportStatus(inst, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(value, "value")) + return; + if (filterCount < 0) + throw std::invalid_argument("Argument 'filterCount' must be >= 0."); + *value = window->GetDialog()->ShowSaveFile( + NullToEmpty(title), NullToEmpty(defaultPath), filters, filterCount, NullToEmpty(defaultFileName) + ); + }); +} + +EXPORTED InteropStatus InfiniFrame_ShowMessage( + InfiniFrameWindow* inst, + const AutoString title, + const AutoString text, + const DialogButtons buttons, + const DialogIcon icon, + DialogResult* value +) { + ResetOut(value, DialogResult::Cancel); + return RunWindowExportStatus(inst, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(value, "value")) + return; + *value = window->GetDialog()->ShowMessage(NullToEmpty(title), NullToEmpty(text), buttons, icon); + }); +} +} diff --git a/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Events.cpp b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Events.cpp new file mode 100644 index 000000000..b124e901c --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Events.cpp @@ -0,0 +1,56 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/Exports/Exports.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern "C" { +EXPORTED InteropStatus InfiniFrame_AddCustomSchemeName(InfiniFrameWindow* instance, const AutoString scheme) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureNotNull(scheme, "scheme")) + return; + window->AddCustomSchemeName(scheme); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetAllMonitors(InfiniFrameWindow* instance, const GetAllMonitorsCallback callback) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (callback == nullptr) + throw std::invalid_argument("Argument 'callback' is null."); + window->GetAllMonitors(callback); + }); +} + +EXPORTED InteropStatus InfiniFrame_SetClosingCallback(InfiniFrameWindow* instance, const ClosingCallback callback) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetClosingCallback(callback); }); +} + +EXPORTED InteropStatus InfiniFrame_setClosedClosedCallback(InfiniFrameWindow* instance, const ClosedCallback callback) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetClosedCallback(callback); }); +} + +EXPORTED InteropStatus InfiniFrame_SetFocusInCallback(InfiniFrameWindow* instance, const FocusInCallback callback) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetFocusInCallback(callback); }); +} + +EXPORTED InteropStatus InfiniFrame_SetFocusOutCallback(InfiniFrameWindow* instance, const FocusOutCallback callback) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetFocusOutCallback(callback); }); +} + +EXPORTED InteropStatus InfiniFrame_SetMovedCallback(InfiniFrameWindow* instance, const MovedCallback callback) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetMovedCallback(callback); }); +} + +EXPORTED InteropStatus InfiniFrame_SetResizedCallback(InfiniFrameWindow* instance, const ResizedCallback callback) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetResizedCallback(callback); }); +} + +EXPORTED InteropStatus InfiniFrame_Invoke(InfiniFrameWindow* instance, const ACTION callback) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (callback == nullptr) + throw std::invalid_argument("Argument 'callback' is null."); + window->Invoke(callback); + }); +} +} diff --git a/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Lifecycle.cpp b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Lifecycle.cpp new file mode 100644 index 000000000..7da18814d --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Lifecycle.cpp @@ -0,0 +1,39 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/Exports/Exports.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern "C" { +EXPORTED InteropStatus InfiniFrame_ctor(InfiniFrameInitParams* initParams, InfiniFrameWindow** value) { + ResetOut(value, static_cast(nullptr)); + return RunExportStatus([&] { + if (!EnsureOutNotNull(value, "value")) + return; + if (initParams == nullptr) + throw std::invalid_argument("Argument 'initParams' is null."); + if (initParams->StructSize != static_cast(sizeof(InfiniFrameInitParams))) { + throw std::invalid_argument("InfiniFrameInitParams.Size does not match native struct size."); + } + auto instance = std::make_unique(initParams); + *value = instance.release(); + }); +} + +EXPORTED InteropStatus InfiniFrame_dtor(InfiniFrameWindow* instance) { + return RunExportStatus([&] { + if (!EnsureNotNull(instance, "instance")) + return; + std::unique_ptr guard{instance}; + }); +} + +EXPORTED InteropStatus InfiniFrame_Close(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow* window) { window->Close(); }); +} + +EXPORTED InteropStatus InfiniFrame_WaitForExit(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow* window) { window->WaitForExit(); }); +} +} diff --git a/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Memory.cpp b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Memory.cpp new file mode 100644 index 000000000..fd8396294 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Memory.cpp @@ -0,0 +1,59 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/Exports/Exports.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern "C" { +EXPORTED InteropStatus InfiniFrame_FreeString(AutoString value) { + return RunExportStatus([&] { + if (!EnsureNotNull(value, "value")) + return; +#ifdef _WIN32 + delete[] value; +#elif __linux__ + g_free(value); +#else + free(value); +#endif + }); +} + +EXPORTED InteropStatus InfiniFrame_FreeStringArray(AutoString* values, const int count) { + return RunExportStatus([&] { + if (!EnsureNotNull(values, "values")) + return; + if (count < 0) + throw std::invalid_argument("Argument 'count' must be >= 0."); + for (int i = 0; i < count; ++i) { + if (values[i] != nullptr) { + InfiniFrame_FreeString(values[i]); + } + } +#ifdef _WIN32 + delete[] values; +#elif __linux__ + delete[] values; +#else + free(values); +#endif + }); +} + +EXPORTED InteropStatus InfiniFrame_GetLastErrorMessage(AutoString* value) { + ResetOut(value, static_cast(nullptr)); + if (!EnsureOutNotNull(value, "value")) + return InteropStatus::OutParameterSetToInvalidNull; + + try { + *value = GetLastErrorMessageCopy(); + return InteropStatus::Success; + } catch (const std::exception& ex) { + return infiniframe::exports::detail::TranslateException(ex); + } catch (...) { + infiniframe::exports::detail::SetFailure(InteropStatus::OperationFailed, "Unknown native exception."); + return InteropStatus::OperationFailed; + } +} +} diff --git a/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Platform.cpp b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Platform.cpp new file mode 100644 index 000000000..3befe9168 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Platform.cpp @@ -0,0 +1,49 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/Exports/Exports.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern "C" { +#ifdef _WIN32 +EXPORTED InteropStatus InfiniFrame_register_win32(const HINSTANCE hInstance) { + return RunExportStatus([&] { + if (hInstance == nullptr) + throw std::invalid_argument("Argument 'hInstance' is null."); + InfiniFrameWindow::Register(hInstance); + }); +} + +EXPORTED InteropStatus InfiniFrame_getHwnd_win32(InfiniFrameWindow* instance, HWND* value) { + ResetOut(value, static_cast(nullptr)); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(value, "value")) + return; + *value = window->getHwnd(); + }); +} + +EXPORTED InteropStatus +InfiniFrame_setWebView2RuntimePath_win32(InfiniFrameWindow*, const AutoString webView2RuntimePath) { + return RunExportStatus([&] { + if (!EnsureNotNull(webView2RuntimePath, "webView2RuntimePath")) + return; + InfiniFrameWindow::SetWebView2RuntimePath(webView2RuntimePath); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetNotificationsEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetNotificationsEnabled(enabled); + }); +} +#elif __APPLE__ +EXPORTED InteropStatus InfiniFrame_register_mac() { + return RunExportStatus([] { InfiniFrameWindow::Register(); }); +} +#endif +} diff --git a/src/InfiniFrame.NativeBridge/Native/Exports.Tests.cpp b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Tests.cpp similarity index 73% rename from src/InfiniFrame.NativeBridge/Native/Exports.Tests.cpp rename to src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Tests.cpp index 16bad74a7..afcfb1938 100644 --- a/src/InfiniFrame.NativeBridge/Native/Exports.Tests.cpp +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.Tests.cpp @@ -1,39 +1,62 @@ -#include "Core/InfiniFrame.h" - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/InfiniFrame.h" +#include "Utils/ExportGuards.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- #ifdef _WIN32 #define EXPORTED __declspec(dllexport) #else #define EXPORTED #endif +#if defined(INFINIFRAME_BUILD_TEST_EXPORTS) + +using infiniframe::exports::EnsureNotNull; +using infiniframe::exports::RunExportStatus; + #ifdef _WIN32 inline AutoString duplicateString(const AutoStringConst str) { - if (str == nullptr) + if (str == nullptr) { return nullptr; + } + const size_t len = wcslen(str); auto* copy = new wchar_t[len + 1]; wcscpy_s(copy, len + 1, str); return copy; } #else -inline AutoString duplicateString(AutoStringConst str) { - if (str == nullptr) +inline AutoString duplicateString(const AutoStringConst str) { + if (str == nullptr) { return nullptr; + } + const size_t len = strlen(str); - auto copy = new char[len + 1]; + auto* copy = new char[len + 1]; strcpy(copy, str); return copy; } #endif extern "C" { - EXPORTED void InfiniWindowTests_NativeParametersReturnAsIs( - const InfiniFrameInitParams* params, - InfiniFrameInitParams** new_params - ) { +EXPORTED InteropStatus InfiniFrameNativeTests_NativeParametersReturnAsIs( + const InfiniFrameInitParams* params, InfiniFrameInitParams** new_params +) { + if (new_params != nullptr) { + *new_params = nullptr; + } + + return RunExportStatus([&] { + if (!EnsureNotNull(params, "params") || + !EnsureNotNull(new_params, "new_params", ::InteropStatus::OutParameterSetToInvalidNull)) { + return; + } + *new_params = new InfiniFrameInitParams(); - // Deep copy AutoString fields (*new_params)->StartString = duplicateString(params->StartString); (*new_params)->StartUrl = duplicateString(params->StartUrl); (*new_params)->Title = duplicateString(params->Title); @@ -43,7 +66,6 @@ extern "C" { (*new_params)->BrowserControlInitParameters = duplicateString(params->BrowserControlInitParameters); (*new_params)->NotificationRegistrationId = duplicateString(params->NotificationRegistrationId); - // Copy the rest using memcpy (copy everything except the strings we already handled) (*new_params)->ParentInstance = params->ParentInstance; (*new_params)->ClosingHandler = params->ClosingHandler; (*new_params)->ClosedHandler = params->ClosedHandler; @@ -56,11 +78,8 @@ extern "C" { (*new_params)->MovedHandler = params->MovedHandler; (*new_params)->WebMessageReceivedHandler = params->WebMessageReceivedHandler; (*new_params)->CustomSchemeHandler = params->CustomSchemeHandler; - - // Copy array memcpy((*new_params)->CustomSchemeNames, params->CustomSchemeNames, sizeof(params->CustomSchemeNames)); - // Copy all numeric and bool fields in one go (*new_params)->Left = params->Left; (*new_params)->Top = params->Top; (*new_params)->Width = params->Width; @@ -92,14 +111,16 @@ extern "C" { (*new_params)->SmoothScrollingEnabled = params->SmoothScrollingEnabled; (*new_params)->IgnoreCertificateErrorsEnabled = params->IgnoreCertificateErrorsEnabled; (*new_params)->NotificationsEnabled = params->NotificationsEnabled; - (*new_params)->Size = params->Size; - } + (*new_params)->StructSize = params->StructSize; + }); +} - EXPORTED void InfiniWindowTests_FreeInitParams(InfiniFrameInitParams* params) { - if (params == nullptr) +EXPORTED InteropStatus InfiniFrameNativeTests_FreeInitParams(InfiniFrameInitParams* params) { + return RunExportStatus([&] { + if (!EnsureNotNull(params, "params")) { return; + } - // Free only string fields that this test export duplicated. delete[] params->StartString; delete[] params->StartUrl; delete[] params->Title; @@ -110,5 +131,8 @@ extern "C" { delete[] params->NotificationRegistrationId; delete params; - } + }); +} } + +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.WindowCommands.cpp b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.WindowCommands.cpp new file mode 100644 index 000000000..4d3819fd8 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.WindowCommands.cpp @@ -0,0 +1,119 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/Exports/Exports.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern "C" { +EXPORTED InteropStatus InfiniFrame_Center(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow* window) { window->Center(); }); +} + +EXPORTED InteropStatus InfiniFrame_ClearBrowserAutoFill(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow* window) { window->ClearBrowserAutoFill(); }); +} + +EXPORTED InteropStatus InfiniFrame_NavigateToString(InfiniFrameWindow* instance, const AutoString content) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureNotNull(content, "content")) + return; + window->NavigateToString(content); + }); +} + +EXPORTED InteropStatus InfiniFrame_NavigateToUrl(InfiniFrameWindow* instance, const AutoString url) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureNotNull(url, "url")) + return; + window->NavigateToUrl(url); + }); +} + +EXPORTED InteropStatus InfiniFrame_Restore(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow* window) { window->Restore(); }); +} + +EXPORTED InteropStatus InfiniFrame_SendWebMessage(InfiniFrameWindow* instance, const AutoString message) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + window->SendWebMessage(NullToEmpty(message)); + }); +} + +EXPORTED InteropStatus InfiniFrame_SetTransparentEnabled(InfiniFrameWindow* instance, const bool enabled) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetTransparentEnabled(enabled); }); +} + +EXPORTED InteropStatus InfiniFrame_SetContextMenuEnabled(InfiniFrameWindow* instance, const bool enabled) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetContextMenuEnabled(enabled); }); +} + +EXPORTED InteropStatus InfiniFrame_SetZoomEnabled(InfiniFrameWindow* instance, const bool enabled) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetZoomEnabled(enabled); }); +} + +EXPORTED InteropStatus InfiniFrame_SetDevToolsEnabled(InfiniFrameWindow* instance, const bool enabled) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetDevToolsEnabled(enabled); }); +} + +EXPORTED InteropStatus InfiniFrame_SetFullScreen(InfiniFrameWindow* instance, const bool fullScreen) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetFullScreen(fullScreen); }); +} + +EXPORTED InteropStatus InfiniFrame_SetIconFile(InfiniFrameWindow* instance, const AutoString filename) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + window->SetIconFile(NullToEmpty(filename)); + }); +} + +EXPORTED InteropStatus InfiniFrame_SetMaximized(InfiniFrameWindow* instance, const bool maximized) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetMaximized(maximized); }); +} + +EXPORTED InteropStatus InfiniFrame_SetMaxSize(InfiniFrameWindow* instance, const int width, const int height) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetMaxSize(width, height); }); +} + +EXPORTED InteropStatus InfiniFrame_SetMinimized(InfiniFrameWindow* instance, const bool minimized) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetMinimized(minimized); }); +} + +EXPORTED InteropStatus InfiniFrame_SetMinSize(InfiniFrameWindow* instance, const int width, const int height) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetMinSize(width, height); }); +} + +EXPORTED InteropStatus InfiniFrame_SetPosition(InfiniFrameWindow* instance, const int x, const int y) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetPosition(x, y); }); +} + +EXPORTED InteropStatus InfiniFrame_SetResizable(InfiniFrameWindow* instance, const bool resizable) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetResizable(resizable); }); +} + +EXPORTED InteropStatus InfiniFrame_SetSize(InfiniFrameWindow* instance, const int width, const int height) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetSize(width, height); }); +} + +EXPORTED InteropStatus InfiniFrame_SetTitle(InfiniFrameWindow* instance, const AutoString title) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetTitle(NullToEmpty(title)); }); +} + +EXPORTED InteropStatus InfiniFrame_SetTopmost(InfiniFrameWindow* instance, const bool topmost) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetTopmost(topmost); }); +} + +EXPORTED InteropStatus InfiniFrame_SetZoom(InfiniFrameWindow* instance, const int zoom) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetZoom(zoom); }); +} + +EXPORTED InteropStatus +InfiniFrame_ShowNotification(InfiniFrameWindow* instance, const AutoString title, const AutoString body) { + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + window->ShowNotification(NullToEmpty(title), NullToEmpty(body)); + }); +} + +EXPORTED InteropStatus InfiniFrame_SetFocused(InfiniFrameWindow* instance) { + return RunWindowExportStatus(instance, [](InfiniFrameWindow* window) { window->SetFocused(); }); +} +} diff --git a/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.WindowState.cpp b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.WindowState.cpp new file mode 100644 index 000000000..da8760ee6 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.WindowState.cpp @@ -0,0 +1,251 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/Exports/Exports.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern "C" { +EXPORTED InteropStatus InfiniFrame_GetTransparentEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetTransparentEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetContextMenuEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetContextMenuEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetZoomEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetZoomEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetDevToolsEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetDevToolsEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetFullScreen(InfiniFrameWindow* instance, bool* fullScreen) { + ResetOut(fullScreen, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(fullScreen, "fullScreen")) + return; + window->GetFullScreen(fullScreen); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetGrantBrowserPermissions(InfiniFrameWindow* instance, bool* grant) { + ResetOut(grant, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(grant, "grant")) + return; + window->GetGrantBrowserPermissions(grant); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetUserAgent(InfiniFrameWindow* instance, AutoString* value) { + ResetOut(value, static_cast(nullptr)); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(value, "value")) + return; + *value = window->GetUserAgent(); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetMediaAutoplayEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetMediaAutoplayEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetFileSystemAccessEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetFileSystemAccessEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetWebSecurityEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetWebSecurityEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetJavascriptClipboardAccessEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetJavascriptClipboardAccessEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetMediaStreamEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetMediaStreamEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetSmoothScrollingEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetSmoothScrollingEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetMaximized(InfiniFrameWindow* instance, bool* isMaximized) { + ResetOut(isMaximized, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(isMaximized, "isMaximized")) + return; + window->GetMaximized(isMaximized); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetMinimized(InfiniFrameWindow* instance, bool* isMinimized) { + ResetOut(isMinimized, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(isMinimized, "isMinimized")) + return; + window->GetMinimized(isMinimized); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetIgnoreCertificateErrorsEnabled(InfiniFrameWindow* instance, bool* enabled) { + ResetOut(enabled, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(enabled, "enabled")) + return; + window->GetIgnoreCertificateErrorsEnabled(enabled); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetPosition(InfiniFrameWindow* instance, int* x, int* y) { + ResetOut2(x, y, 0); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(x, "x") || !EnsureOutNotNull(y, "y")) + return; + window->GetPosition(x, y); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetResizable(InfiniFrameWindow* instance, bool* resizable) { + ResetOut(resizable, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(resizable, "resizable")) + return; + window->GetResizable(resizable); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetScreenDpi(InfiniFrameWindow* instance, unsigned int* value) { + ResetOut(value, static_cast(0)); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(value, "value")) + return; + *value = window->GetScreenDpi(); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetSize(InfiniFrameWindow* instance, int* width, int* height) { + ResetOut2(width, height, 0); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(width, "width") || !EnsureOutNotNull(height, "height")) + return; + window->GetSize(width, height); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetMaxSize(InfiniFrameWindow* instance, int* width, int* height) { + ResetOut2(width, height, 0); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(width, "width") || !EnsureOutNotNull(height, "height")) + return; + window->GetMaxSize(width, height); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetMinSize(InfiniFrameWindow* instance, int* width, int* height) { + ResetOut2(width, height, 0); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(width, "width") || !EnsureOutNotNull(height, "height")) + return; + window->GetMinSize(width, height); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetTitle(InfiniFrameWindow* instance, AutoString* value) { + ResetOut(value, static_cast(nullptr)); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(value, "value")) + return; + *value = window->GetTitle(); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetTopmost(InfiniFrameWindow* instance, bool* topmost) { + ResetOut(topmost, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(topmost, "topmost")) + return; + window->GetTopmost(topmost); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetZoom(InfiniFrameWindow* instance, int* zoom) { + ResetOut(zoom, 0); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(zoom, "zoom")) + return; + window->GetZoom(zoom); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetFocused(InfiniFrameWindow* instance, bool* isFocused) { + ResetOut(isFocused, false); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(isFocused, "isFocused")) + return; + window->GetFocused(isFocused); + }); +} + +EXPORTED InteropStatus InfiniFrame_GetIconFileName(InfiniFrameWindow* instance, AutoString* value) { + ResetOut(value, static_cast(nullptr)); + return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { + if (!EnsureOutNotNull(value, "value")) + return; + *value = window->GetIconFileName(); + }); +} +} diff --git a/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.h b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.h new file mode 100644 index 000000000..03cb07d37 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/Exports/Exports.h @@ -0,0 +1,40 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#ifdef __linux__ +#include +#endif + +#ifdef _WIN32 +#define EXPORTED __declspec(dllexport) +#else +#define EXPORTED +#endif + +#include "Public/InfiniFrame.h" +#include "Utils/ExportGuards.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +using infiniframe::exports::EnsureNotNull; +using infiniframe::exports::GetLastErrorMessageCopy; +using infiniframe::exports::ResetOut; +using infiniframe::exports::ResetOut2; +using infiniframe::exports::RunExportStatus; +using infiniframe::exports::RunReturnExport; +using infiniframe::exports::RunWindowExportStatus; +using infiniframe::exports::RunWindowReturnExport; + +template bool EnsureOutNotNull(T* value, const char* argumentName) noexcept { + return infiniframe::exports::EnsureNotNull(value, argumentName, InteropStatus::OutParameterSetToInvalidNull); +} + +inline AutoString NullToEmpty(const AutoString value) noexcept { +#ifdef _WIN32 + static const wchar_t empty[] = L""; +#else + static const char empty[] = ""; +#endif + return value != nullptr ? value : const_cast(empty); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrame.h b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrame.h new file mode 100644 index 000000000..5f00610bf --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrame.h @@ -0,0 +1,14 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Public/InfiniFrameInitParams.h" +#include "Public/InfiniFrameWindow.h" +#include "Public/InfiniFrameDialog.h" + +#include "Types/Basic.h" +#include "Types/Dialog.h" +#include "Types/Callbacks.h" + +#include "Utils/Common.h" +#include "Utils/Event.h" diff --git a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameDialog.h b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameDialog.h similarity index 55% rename from src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameDialog.h rename to src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameDialog.h index 880fce5dd..cb50c9d7f 100644 --- a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameDialog.h +++ b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameDialog.h @@ -1,19 +1,16 @@ #pragma once -/** - * @file InfiniFrameDialog.h - * @brief Dialog handlers for file/folder operations and messages - */ - -#ifndef INFINIFRAME_CORE_DIALOG_H -#define INFINIFRAME_CORE_DIALOG_H - -#include "../Types/Basic.h" -#include "../Types/Dialog.h" - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #ifdef __APPLE__ #include #endif +#include "Types/Basic.h" +#include "Types/Dialog.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- class InfiniFrameWindow; // forward declaration /** @@ -22,24 +19,24 @@ class InfiniFrameWindow; // forward declaration class InfiniFrameDialog { public: #ifdef _WIN32 - /** + /** * @brief Construct dialog handler with parent window (Windows) * @param window Parent InfiniFrame window */ - InfiniFrameDialog(InfiniFrameWindow* window); + InfiniFrameDialog(InfiniFrameWindow* window); #else - /** + /** * @brief Construct dialog handler (Linux/macOS) */ - InfiniFrameDialog(); + InfiniFrameDialog(); #endif - /** + /** * @brief Destroy dialog handler */ - ~InfiniFrameDialog(); + ~InfiniFrameDialog(); - /** + /** * @brief Show open file dialog * @param title Dialog title * @param defaultPath Default path @@ -49,16 +46,16 @@ class InfiniFrameDialog { * @param resultCount Output: number of selected files * @return Array of selected file paths */ - AutoString* ShowOpenFile( - AutoString title, - AutoString defaultPath, - bool multiSelect, - AutoString* filters, - int filterCount, - int* resultCount - ); + AutoString* ShowOpenFile( + AutoString title, + AutoString defaultPath, + bool multiSelect, + AutoString* filters, + int filterCount, + int* resultCount + ); - /** + /** * @brief Show open folder dialog * @param title Dialog title * @param defaultPath Default path @@ -66,9 +63,9 @@ class InfiniFrameDialog { * @param resultCount Output: number of selected folders * @return Array of selected folder paths */ - AutoString* ShowOpenFolder(AutoString title, AutoString defaultPath, bool multiSelect, int* resultCount); + AutoString* ShowOpenFolder(AutoString title, AutoString defaultPath, bool multiSelect, int* resultCount); - /** + /** * @brief Show save file dialog * @param title Dialog title * @param defaultPath Default path @@ -77,15 +74,15 @@ class InfiniFrameDialog { * @param defaultFileName Default file name * @return Selected file path */ - AutoString ShowSaveFile( - AutoString title, - AutoString defaultPath, - AutoString* filters, - int filterCount, - AutoString defaultFileName = nullptr - ); + AutoString ShowSaveFile( + AutoString title, + AutoString defaultPath, + AutoString* filters, + int filterCount, + AutoString defaultFileName = nullptr + ); - /** + /** * @brief Show message dialog * @param title Dialog title * @param text Message text @@ -93,17 +90,15 @@ class InfiniFrameDialog { * @param icon Icon type * @return User's response */ - DialogResult ShowMessage(AutoString title, AutoString text, DialogButtons buttons, DialogIcon icon); + DialogResult ShowMessage(AutoString title, AutoString text, DialogButtons buttons, DialogIcon icon); protected: #ifdef __APPLE__ - NSImage* _errorIcon; - NSImage* _infoIcon; - NSImage* _questionIcon; - NSImage* _warningIcon; + NSImage* _errorIcon; + NSImage* _infoIcon; + NSImage* _questionIcon; + NSImage* _warningIcon; #elif _WIN32 - InfiniFrameWindow* _window; + InfiniFrameWindow* _window; #endif }; - -#endif // INFINIFRAME_CORE_DIALOG_H diff --git a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameInitParams.h b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameInitParams.h similarity index 71% rename from src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameInitParams.h rename to src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameInitParams.h index e4f9a23de..030d73e2c 100644 --- a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameInitParams.h +++ b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameInitParams.h @@ -1,21 +1,20 @@ #pragma once -/** - * @file InfiniFrameInitParams.h - * @brief Window initialization parameters - */ - -#ifndef INFINIFRAME_CORE_INITPARAMS_H -#define INFINIFRAME_CORE_INITPARAMS_H - -#include "../Types/Basic.h" -#include "../Types/Callbacks.h" - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Types/Basic.h" +#include "Types/Callbacks.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- class InfiniFrameWindow; // Forward declaration /** * @brief Initialization parameters for InfiniFrame window */ struct InfiniFrameInitParams { + static constexpr std::size_t MaxCustomSchemeNames = 16; + // Content AutoString StartString; AutoString StartUrl; @@ -42,7 +41,7 @@ struct InfiniFrameInitParams { MinimizedCallback MinimizedHandler; MovedCallback MovedHandler; WebMessageReceivedCallback WebMessageReceivedHandler; - AutoString CustomSchemeNames[16]; + AutoString CustomSchemeNames[MaxCustomSchemeNames]; // NOLINT(*-avoid-c-arrays) WebResourceRequestedCallback CustomSchemeHandler; // Position and size @@ -81,7 +80,5 @@ struct InfiniFrameInitParams { bool NotificationsEnabled; // Struct size (for version checking) - int Size; + int StructSize; }; - -#endif // INFINIFRAME_CORE_INITPARAMS_H diff --git a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameWindow.h b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameWindow.h similarity index 57% rename from src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameWindow.h rename to src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameWindow.h index eca5cca3d..dd789524c 100644 --- a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameWindow.h +++ b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameWindow.h @@ -1,17 +1,11 @@ #pragma once -/** - * @file InfiniFrameWindow.h - * @brief Main window class for InfiniFrame - */ - -#ifndef INFINIFRAME_CORE_WINDOW_H -#define INFINIFRAME_CORE_WINDOW_H - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #ifdef _WIN32 #include #include #include -class WinToastHandler; #endif #ifdef __APPLE__ @@ -33,10 +27,15 @@ class WinToastHandler; #include #include -#include "../Types/Basic.h" -#include "../Types/Dialog.h" -#include "../Types/Callbacks.h" - +#include "Types/Basic.h" +#include "Types/Dialog.h" +#include "Types/Callbacks.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +#ifdef _WIN32 +class WinToastHandler; +#endif class InfiniFrameDialog; struct InfiniFrameInitParams; @@ -48,556 +47,554 @@ struct InfiniFrameInitParams; */ class InfiniFrameWindow { public: - /** + /** * @brief Construct new InfiniFrame window * @param initParams Initialization parameters */ - explicit InfiniFrameWindow(InfiniFrameInitParams* initParams); + explicit InfiniFrameWindow(InfiniFrameInitParams* initParams); - /** + /** * @brief Destroy InfiniFrame window */ - ~InfiniFrameWindow(); + ~InfiniFrameWindow(); - /** + /** * @brief Get dialog handler * @return Pointer to InfiniFrameDialog */ - [[nodiscard]] InfiniFrameDialog* GetDialog() const; + [[nodiscard]] InfiniFrameDialog* GetDialog() const; - // ----------------------------------------------------------------------------------------------------------------- - // Window Operations - // ----------------------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------------------------- + // Window Operations + // ----------------------------------------------------------------------------------------------------------------- - /** @brief Center the window on the current screen */ - void Center(); + /** @brief Center the window on the current screen */ + void Center(); - /** @brief Clear all browser autofill data (passwords, forms) */ - void ClearBrowserAutoFill(); + /** @brief Clear all browser autofill data (passwords, forms) */ + void ClearBrowserAutoFill(); - /** @brief Close the window and terminate the message loop */ - void Close(); + /** @brief Close the window and terminate the message loop */ + void Close(); - // ----------------------------------------------------------------------------------------------------------------- - // Get Properties - // ----------------------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------------------------- + // Get Properties + // ----------------------------------------------------------------------------------------------------------------- - /** + /** * @brief Get whether transparent background is enabled * @param enabled Output: true if transparent background is active */ - void GetTransparentEnabled(bool* enabled) const; + void GetTransparentEnabled(bool* enabled) const; - /** + /** * @brief Get whether the browser context menu is enabled * @param enabled Output: true if context menu is shown on right-click */ - void GetContextMenuEnabled(bool* enabled) const; + void GetContextMenuEnabled(bool* enabled) const; - /** + /** * @brief Get whether user-controlled zoom is enabled * @param enabled Output: true if the user can zoom via keyboard/mouse */ - void GetZoomEnabled(bool* enabled) const; + void GetZoomEnabled(bool* enabled) const; - /** + /** * @brief Get whether the browser DevTools panel is enabled * @param enabled Output: true if DevTools can be opened */ - void GetDevToolsEnabled(bool* enabled) const; + void GetDevToolsEnabled(bool* enabled) const; - /** + /** * @brief Get whether the window is in fullscreen mode * @param fullScreen Output: true if the window occupies the full screen */ - void GetFullScreen(bool* fullScreen) const; + void GetFullScreen(bool* fullScreen) const; - /** + /** * @brief Get whether browser permission requests are auto-granted * @param grant Output: true if permissions (camera, microphone, etc.) are granted without prompting */ - void GetGrantBrowserPermissions(bool* grant) const; + void GetGrantBrowserPermissions(bool* grant) const; - /** + /** * @brief Get the custom user-agent string * @return UTF-8 user-agent string; caller must free with InfiniFrame_FreeString */ - [[nodiscard]] AutoString GetUserAgent() const; + [[nodiscard]] AutoString GetUserAgent() const; - /** + /** * @brief Get whether media autoplay is enabled * @param enabled Output: true if audio/video may autoplay without user interaction */ - void GetMediaAutoplayEnabled(bool* enabled) const; + void GetMediaAutoplayEnabled(bool* enabled) const; - /** + /** * @brief Get whether the File System Access API is enabled * @param enabled Output: true if web content may access the local file system */ - void GetFileSystemAccessEnabled(bool* enabled) const; + void GetFileSystemAccessEnabled(bool* enabled) const; - /** + /** * @brief Get whether web security (same-origin / CORS) is enabled * @param enabled Output: true if standard web security restrictions are enforced */ - void GetWebSecurityEnabled(bool* enabled) const; + void GetWebSecurityEnabled(bool* enabled) const; - /** + /** * @brief Get whether JavaScript clipboard read/write access is enabled * @param enabled Output: true if the Clipboard API is accessible from scripts */ - void GetJavascriptClipboardAccessEnabled(bool* enabled) const; + void GetJavascriptClipboardAccessEnabled(bool* enabled) const; - /** + /** * @brief Get whether the MediaStream API is enabled * @param enabled Output: true if camera/microphone streaming is permitted */ - void GetMediaStreamEnabled(bool* enabled) const; + void GetMediaStreamEnabled(bool* enabled) const; - /** + /** * @brief Get whether smooth scrolling is enabled * @param enabled Output: true if CSS smooth-scroll behaviour is active */ - void GetSmoothScrollingEnabled(bool* enabled) const; + void GetSmoothScrollingEnabled(bool* enabled) const; - /** + /** * @brief Get the window icon file path * @return UTF-8 path to the icon file; caller must free with InfiniFrame_FreeString */ - [[nodiscard]] AutoString GetIconFileName() const; + [[nodiscard]] AutoString GetIconFileName() const; - /** + /** * @brief Get whether the window is maximized * @param isMaximized Output: true if the window is currently maximized */ - void GetMaximized(bool* isMaximized) const; + void GetMaximized(bool* isMaximized) const; - /** + /** * @brief Get whether the window is minimized * @param isMinimized Output: true if the window is currently minimized */ - void GetMinimized(bool* isMinimized) const; + void GetMinimized(bool* isMinimized) const; - /** + /** * @brief Get the window position in screen coordinates * @param x Output: left edge position in pixels * @param y Output: top edge position in pixels */ - void GetPosition(int* x, int* y) const; + void GetPosition(int* x, int* y) const; - /** + /** * @brief Get whether the window can be resized by the user * @param resizable Output: true if the window has a resizable border */ - void GetResizable(bool* resizable) const; + void GetResizable(bool* resizable) const; - /** + /** * @brief Get the DPI of the screen the window is on * @return DPI value (e.g. 96 for 100%, 192 for 200%) */ - [[nodiscard]] unsigned int GetScreenDpi() const; + [[nodiscard]] unsigned int GetScreenDpi() const; - /** + /** * @brief Get the current window size * @param width Output: client-area width in pixels * @param height Output: client-area height in pixels */ - void GetSize(int* width, int* height) const; + void GetSize(int* width, int* height) const; - /** + /** * @brief Get the maximum allowed window size * @param width Output: maximum width in pixels * @param height Output: maximum height in pixels */ - void GetMaxSize(int* width, int* height) const; + void GetMaxSize(int* width, int* height) const; - /** + /** * @brief Get the minimum allowed window size * @param width Output: minimum width in pixels * @param height Output: minimum height in pixels */ - void GetMinSize(int* width, int* height) const; + void GetMinSize(int* width, int* height) const; - /** + /** * @brief Get the window title bar text * @return UTF-8 title string; caller must free with InfiniFrame_FreeString */ - [[nodiscard]] AutoString GetTitle() const; + [[nodiscard]] AutoString GetTitle() const; - /** + /** * @brief Get whether the window is always on top of other windows * @param topmost Output: true if the always-on-top flag is set */ - void GetTopmost(bool* topmost) const; + void GetTopmost(bool* topmost) const; - /** + /** * @brief Get the current zoom level * @param zoom Output: zoom percentage (100 = 100%) */ - void GetZoom(int* zoom) const; + void GetZoom(int* zoom) const; - /** + /** * @brief Get whether TLS certificate errors are silently ignored * @param enabled Output: true if certificate errors are suppressed */ - void GetIgnoreCertificateErrorsEnabled(bool* enabled) const; + void GetIgnoreCertificateErrorsEnabled(bool* enabled) const; - /** + /** * @brief Get whether the window currently has keyboard focus * @param isFocused Output: true if the window is the foreground window */ - void GetFocused(bool* isFocused) const; + void GetFocused(bool* isFocused) const; - // ----------------------------------------------------------------------------------------------------------------- - // Navigation - // ----------------------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------------------------- + // Navigation + // ----------------------------------------------------------------------------------------------------------------- - /** + /** * @brief Load HTML content directly from a string * @param content UTF-8 HTML source to display */ - void NavigateToString(AutoString content); + void NavigateToString(AutoString content); - /** + /** * @brief Navigate the WebView to a URL * @param url UTF-8 URL to load (http/https or custom scheme) */ - void NavigateToUrl(AutoString url); + void NavigateToUrl(AutoString url); - /** @brief Restore the window from a minimized or maximized state */ - void Restore(); + /** @brief Restore the window from a minimized or maximized state */ + void Restore(); - /** + /** * @brief Post a message string to the web content (received via window.chrome.webview.addEventListener) * @param message UTF-8 message payload */ - void SendWebMessage(AutoString message); + void SendWebMessage(AutoString message); - // ----------------------------------------------------------------------------------------------------------------- - // Set Properties - // ----------------------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------------------------- + // Set Properties + // ----------------------------------------------------------------------------------------------------------------- - /** + /** * @brief Enable or disable transparent window background * @param enabled true to enable transparency */ - void SetTransparentEnabled(bool enabled); + void SetTransparentEnabled(bool enabled); - /** + /** * @brief Enable or disable the browser right-click context menu * @param enabled true to show the context menu */ - void SetContextMenuEnabled(bool enabled); + void SetContextMenuEnabled(bool enabled); - /** + /** * @brief Enable or disable user-controlled zoom * @param enabled true to allow pinch/keyboard zoom */ - void SetZoomEnabled(bool enabled); + void SetZoomEnabled(bool enabled); - /** + /** * @brief Enable or disable the browser DevTools panel * @param enabled true to make DevTools accessible */ - void SetDevToolsEnabled(bool enabled); + void SetDevToolsEnabled(bool enabled); - /** + /** * @brief Set the window icon from a file * @param filename UTF-8 path to an image file */ - void SetIconFile(AutoString filename); + void SetIconFile(AutoString filename); - /** + /** * @brief Enter or exit fullscreen mode * @param fullScreen true to go fullscreen, false to restore */ - void SetFullScreen(bool fullScreen); + void SetFullScreen(bool fullScreen); - /** + /** * @brief Maximize or unmaximize the window * @param maximized true to maximize */ - void SetMaximized(bool maximized); + void SetMaximized(bool maximized); - /** + /** * @brief Set the maximum allowed window size * @param width Maximum width in pixels (0 = unlimited) * @param height Maximum height in pixels (0 = unlimited) */ - void SetMaxSize(int width, int height); + void SetMaxSize(int width, int height); - /** + /** * @brief Minimize or restore the window * @param minimized true to minimize */ - void SetMinimized(bool minimized); + void SetMinimized(bool minimized); - /** + /** * @brief Set the minimum allowed window size * @param width Minimum width in pixels * @param height Minimum height in pixels */ - void SetMinSize(int width, int height); + void SetMinSize(int width, int height); - /** + /** * @brief Move the window to screen coordinates * @param x Left edge position in pixels * @param y Top edge position in pixels */ - void SetPosition(int x, int y); + void SetPosition(int x, int y); - /** + /** * @brief Enable or disable user resizing via window border * @param resizable true to allow resizing */ - void SetResizable(bool resizable); + void SetResizable(bool resizable); - /** + /** * @brief Resize the window * @param width New width in pixels * @param height New height in pixels */ - void SetSize(int width, int height); + void SetSize(int width, int height); - /** + /** * @brief Set the window title bar text * @param title UTF-8 title string */ - void SetTitle(AutoString title); + void SetTitle(AutoString title); - /** + /** * @brief Pin or unpin the window above all other windows * @param topmost true to keep always on top */ - void SetTopmost(bool topmost); + void SetTopmost(bool topmost); - /** + /** * @brief Set the WebView zoom level * @param zoom Zoom percentage (e.g. 100 for 100%, 150 for 150%) */ - void SetZoom(int zoom); + void SetZoom(int zoom); - /** @brief Move keyboard focus into the window */ - void SetFocused(); + /** @brief Move keyboard focus into the window */ + void SetFocused(); - // ----------------------------------------------------------------------------------------------------------------- - // Notifications - // ----------------------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------------------------- + // Notifications + // ----------------------------------------------------------------------------------------------------------------- - /** + /** * @brief Show a native system notification (toast on Windows, libnotify on Linux, UNUserNotification on macOS) * @param title UTF-8 notification title * @param message UTF-8 notification body text */ - void ShowNotification(AutoString title, AutoString message); + void ShowNotification(AutoString title, AutoString message); - /** + /** * @brief Block the calling thread until the window is closed; runs the platform message loop. * Must be called from the thread that created the window. */ - void WaitForExit(); + void WaitForExit(); - /** @brief Tear down the WebView control while keeping the native window alive */ - void CloseWebView(); + /** @brief Tear down the WebView control while keeping the native window alive */ + void CloseWebView(); - // ----------------------------------------------------------------------------------------------------------------- - // Callbacks - // ----------------------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------------------------- + // Callbacks + // ----------------------------------------------------------------------------------------------------------------- - /** + /** * @brief Register a custom URI scheme to be intercepted by WebResourceRequestedCallback * @param scheme UTF-8 scheme name without "://" (e.g. "app") */ - void AddCustomSchemeName(AutoStringConst scheme); + void AddCustomSchemeName(AutoStringConst scheme); - /** + /** * @brief Enumerate all connected monitors by invoking a callback for each one * @param callback Called once per monitor; receives a Monitor describing geometry and scale */ - void GetAllMonitors(GetAllMonitorsCallback callback) const; + void GetAllMonitors(GetAllMonitorsCallback callback) const; - /** + /** * @brief Set callback invoked when the user attempts to close the window * @param callback Returns true to allow closing, false to cancel */ - void SetClosingCallback(ClosingCallback callback); - - /** + void SetClosingCallback(ClosingCallback callback); + + /** * @brief Set callback invoked when the window is closed * @param callback Invoked with no arguments */ - void SetClosedCallback(ClosedCallback callback); + void SetClosedCallback(ClosedCallback callback); - /** + /** * @brief Set callback invoked when the window gains keyboard focus * @param callback Invoked with no arguments */ - void SetFocusInCallback(FocusInCallback callback); + void SetFocusInCallback(FocusInCallback callback); - /** + /** * @brief Set callback invoked when the window loses keyboard focus * @param callback Invoked with no arguments */ - void SetFocusOutCallback(FocusOutCallback callback); + void SetFocusOutCallback(FocusOutCallback callback); - /** + /** * @brief Set callback invoked when the window is moved * @param callback Receives new (x, y) screen coordinates */ - void SetMovedCallback(MovedCallback callback); + void SetMovedCallback(MovedCallback callback); - /** + /** * @brief Set callback invoked when the window is resized * @param callback Receives new (width, height) in pixels */ - void SetResizedCallback(ResizedCallback callback); + void SetResizedCallback(ResizedCallback callback); - /** + /** * @brief Set callback invoked when the window is maximized * @param callback Invoked with no arguments */ - void SetMaximizedCallback(MaximizedCallback callback); + void SetMaximizedCallback(MaximizedCallback callback); - /** + /** * @brief Set callback invoked when the window is restored from maximized state * @param callback Invoked with no arguments */ - void SetRestoredCallback(RestoredCallback callback); + void SetRestoredCallback(RestoredCallback callback); - /** + /** * @brief Set callback invoked when the window is minimized * @param callback Invoked with no arguments */ - void SetMinimizedCallback(MinimizedCallback callback); + void SetMinimizedCallback(MinimizedCallback callback); - /** + /** * @brief Marshal a callback onto the UI thread and execute it synchronously * @param callback Action to invoke on the UI thread */ - void Invoke(ACTION callback); + void Invoke(ACTION callback); - /** + /** * @brief Fire the closing callback * @return true if the window should close, false if the callback cancelled it */ - [[nodiscard]] bool InvokeClose() const noexcept; - - /** @brief Fire the close callback */ - void InvokeClosed() const noexcept; + [[nodiscard]] bool InvokeClose() const noexcept; + + /** @brief Fire the close callback */ + void InvokeClosed() const noexcept; - /** @brief Fire the focus-in callback */ - void InvokeFocusIn() const noexcept; + /** @brief Fire the focus-in callback */ + void InvokeFocusIn() const noexcept; - /** @brief Fire the focus-out callback */ - void InvokeFocusOut() const noexcept; + /** @brief Fire the focus-out callback */ + void InvokeFocusOut() const noexcept; - /** + /** * @brief Fire the moved callback * @param x New left edge in screen pixels * @param y New top edge in screen pixels */ - void InvokeMove(int x, int y) const noexcept; + void InvokeMove(int x, int y) const noexcept; - /** + /** * @brief Fire the resized callback * @param width New width in pixels * @param height New height in pixels */ - void InvokeResize(int width, int height) const noexcept; + void InvokeResize(int width, int height) const noexcept; - /** @brief Fire the maximized callback */ - void InvokeMaximized() const noexcept; + /** @brief Fire the maximized callback */ + void InvokeMaximized() const noexcept; - /** @brief Fire the restored callback */ - void InvokeRestored() const noexcept; + /** @brief Fire the restored callback */ + void InvokeRestored() const noexcept; - /** @brief Fire the minimized callback */ - void InvokeMinimized() const noexcept; + /** @brief Fire the minimized callback */ + void InvokeMinimized() const noexcept; - // ----------------------------------------------------------------------------------------------------------------- - // Platform-specific - // ----------------------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------------------------- + // Platform-specific + // ----------------------------------------------------------------------------------------------------------------- #ifdef __linux__ - void OnConfigureEvent(int x, int y, int width, int height); - void OnWindowStateEvent(GdkWindowState newState); + void OnConfigureEvent(int x, int y, int width, int height); + void OnWindowStateEvent(GdkWindowState newState); #endif #ifdef _WIN32 - /** + /** * @brief Register the Win32 window class; must be called once before creating any window * @param hInstance Application instance handle */ - static void Register(HINSTANCE hInstance); + static void Register(HINSTANCE hInstance); - /** + /** * @brief Override the WebView2 fixed-version runtime path * @param pathToWebView2 UTF-8 path to the WebView2 runtime directory */ - static void SetWebView2RuntimePath(AutoString pathToWebView2); + static void SetWebView2RuntimePath(AutoString pathToWebView2); - /** + /** * @brief Get the native Win32 window handle * @return HWND for this window */ - HWND getHwnd(); + HWND getHwnd(); - /** @brief Resize the WebView2 control to fill the current client area */ - void RefitContent(); + /** @brief Resize the WebView2 control to fill the current client area */ + void RefitContent(); - /** @brief Move keyboard focus into the WebView2 control */ - void FocusWebView2(); + /** @brief Move keyboard focus into the WebView2 control */ + void FocusWebView2(); - /** @brief Notify WebView2 that the host window has moved (required to update composition) */ - void NotifyWebView2WindowMove(); + /** @brief Notify WebView2 that the host window has moved (required to update composition) */ + void NotifyWebView2WindowMove(); - /** + /** * @brief Get whether Windows toast notifications are available and registered * @param enabled Output: true if WinToast is initialised and ready */ - void GetNotificationsEnabled(bool* enabled) const; + void GetNotificationsEnabled(bool* enabled) const; - /** + /** * @brief Convert a UTF-8 AutoString to a UTF-16 wide string using simdutf * @param source Null-terminated UTF-8 string * @return std::wstring containing the UTF-16 representation */ - std::wstring ToUTF16String(AutoString source) const; + std::wstring ToUTF16String(AutoString source) const; - /** + /** * @brief Convert a UTF-16 AutoString to a UTF-8 std::string using simdutf * @param source Null-terminated UTF-16 string (passed as AutoString / const char*) * @return std::string containing the UTF-8 representation */ - std::string ToUTF8String(AutoString source) const; + std::string ToUTF8String(AutoString source) const; #elif __APPLE__ - /** + /** * @brief Initialise the NSApplication shared instance; must be called once before creating any window */ - static void Register(); + static void Register(); #endif - // ----------------------------------------------------------------------------------------------------------------- - // Private Implementation (Pimpl) - // ----------------------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------------------------- + // Private Implementation (Pimpl) + // ----------------------------------------------------------------------------------------------------------------- private: - void Show(bool isAlreadyShown); - void AttachWebView(); + void Show(bool isAlreadyShown); + void AttachWebView(); #ifdef _WIN32 - static bool EnsureWebViewIsInstalled(); - static bool InstallWebView2(); + static bool EnsureWebViewIsInstalled(); + static bool InstallWebView2(); #endif #ifdef _WIN32 - friend LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + friend LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); #endif - struct Impl; - std::unique_ptr m_impl; + struct Impl; + std::unique_ptr m_impl; }; #include "InfiniFrameInitParams.h" - -#endif // INFINIFRAME_CORE_WINDOW_H diff --git a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameWindowImpl.h b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameWindowImpl.h similarity index 63% rename from src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameWindowImpl.h rename to src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameWindowImpl.h index 6529323e1..97eb28c2a 100644 --- a/src/InfiniFrame.NativeBridge/Native/Core/InfiniFrameWindowImpl.h +++ b/src/InfiniFrame.NativeBridge/Native/Public/InfiniFrameWindowImpl.h @@ -1,35 +1,23 @@ #pragma once -/** - * @file InfiniFrameWindowImpl.h - * @brief Shared state for all platform InfiniFrameWindow::Impl structs. - * - * This is an INTERNAL header — included only by platform Window.cpp/.mm files, - * never by consumers of InfiniFrame. It defines the fields that are identical - * across Windows, Linux, and macOS implementations. - * - * Each platform defines: - * - * struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { ... platform handles ... }; - */ - -#ifndef INFINIFRAME_CORE_WINDOWIMPL_H -#define INFINIFRAME_CORE_WINDOWIMPL_H - -#include "../Types/Basic.h" -#include "../Types/Callbacks.h" -#include "InfiniFrameDialog.h" - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #include #include #include +#include "Types/Basic.h" +#include "Types/Callbacks.h" +#include "Public/InfiniFrameDialog.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- class InfiniFrameWindow; struct InfiniFrameWindowImpl { - // -----------------------------------------------------------------------------------------------------------------= + // ----------------------------------------------------------------------------------------------------------------- // Callbacks - // -----------------------------------------------------------------------------------------------------------------= - + // ----------------------------------------------------------------------------------------------------------------- WebMessageReceivedCallback _webMessageReceivedCallback = nullptr; WebResourceRequestedCallback _customSchemeCallback = nullptr; ResizedCallback _resizedCallback = nullptr; @@ -42,10 +30,9 @@ struct InfiniFrameWindowImpl { FocusInCallback _focusInCallback = nullptr; FocusOutCallback _focusOutCallback = nullptr; - // -----------------------------------------------------------------------------------------------------------------= + // ----------------------------------------------------------------------------------------------------------------- // Feature flags - // -----------------------------------------------------------------------------------------------------------------= - + // ----------------------------------------------------------------------------------------------------------------- bool _transparentEnabled = false; bool _contextMenuEnabled = true; bool _zoomEnabled = true; @@ -59,10 +46,9 @@ struct InfiniFrameWindowImpl { bool _smoothScrollingEnabled = true; bool _ignoreCertificateErrorsEnabled = false; - // -----------------------------------------------------------------------------------------------------------------= - // String state (NativeString = std::wstring on Windows, std::string elsewhere) - // -----------------------------------------------------------------------------------------------------------------= - + // ----------------------------------------------------------------------------------------------------------------- + // String state + // ----------------------------------------------------------------------------------------------------------------- NativeString _windowTitle; NativeString _startUrl; NativeString _startString; @@ -72,12 +58,9 @@ struct InfiniFrameWindowImpl { std::vector _customSchemeNames; - // -----------------------------------------------------------------------------------------------------------------= + // ----------------------------------------------------------------------------------------------------------------- // Ownership - // -----------------------------------------------------------------------------------------------------------------= - + // ----------------------------------------------------------------------------------------------------------------- InfiniFrameWindow* _parent = nullptr; std::unique_ptr _dialog; }; - -#endif // INFINIFRAME_CORE_WINDOWIMPL_H diff --git a/src/InfiniFrame.NativeBridge/Native/Types/Basic.h b/src/InfiniFrame.NativeBridge/Native/Types/Basic.h index b33eccc8f..ab3605974 100644 --- a/src/InfiniFrame.NativeBridge/Native/Types/Basic.h +++ b/src/InfiniFrame.NativeBridge/Native/Types/Basic.h @@ -1,28 +1,17 @@ #pragma once -/** - * @file Basic.h - * @brief Basic type definitions for cross-platform interop - */ - -#ifndef INFINIFRAME_TYPES_BASIC_H -#define INFINIFRAME_TYPES_BASIC_H - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #include - // --------------------------------------------------------------------------------------------------------------------- -// Native String Type +// Code // --------------------------------------------------------------------------------------------------------------------- - #ifdef _WIN32 using NativeString = std::wstring; #else using NativeString = std::string; #endif -// --------------------------------------------------------------------------------------------------------------------- -// AutoString (C API Interop) -// --------------------------------------------------------------------------------------------------------------------- - #ifdef _WIN32 using AutoString = wchar_t*; using AutoStringConst = const wchar_t*; @@ -30,5 +19,3 @@ using AutoStringConst = const wchar_t*; using AutoString = char*; using AutoStringConst = const char*; #endif - -#endif // INFINIFRAME_TYPES_BASIC_H diff --git a/src/InfiniFrame.NativeBridge/Native/Types/Callbacks.h b/src/InfiniFrame.NativeBridge/Native/Types/Callbacks.h index 323c5ec30..b6e6ca453 100644 --- a/src/InfiniFrame.NativeBridge/Native/Types/Callbacks.h +++ b/src/InfiniFrame.NativeBridge/Native/Types/Callbacks.h @@ -1,19 +1,12 @@ #pragma once -/** - * @file Callbacks.h - * @brief C-style callback type definitions for interop - */ - -#ifndef INFINIFRAME_TYPES_CALLBACKS_H -#define INFINIFRAME_TYPES_CALLBACKS_H - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #include "Basic.h" #include "Dialog.h" - // --------------------------------------------------------------------------------------------------------------------- -// C-style Callbacks (for C# interop) +// Code // --------------------------------------------------------------------------------------------------------------------- - /** @brief Generic parameterless action callback */ using ACTION = void (*)(); @@ -32,7 +25,7 @@ using WebMessageReceivedCallback = void (*)(AutoString message, AutoString origi * @param outContentType Output: MIME type string (e.g. "text/html") * @return Heap-allocated response body; ownership is transferred to the caller */ -using WebResourceRequestedCallback = void *(*)(AutoString url, int* outNumBytes, AutoString* outContentType); +using WebResourceRequestedCallback = void* (*)(AutoString url, int* outNumBytes, AutoString* outContentType); /** * @brief Called once per monitor during a GetAllMonitors enumeration. @@ -78,5 +71,3 @@ using FocusInCallback = void (*)(); /** @brief Called when the window loses keyboard focus */ using FocusOutCallback = void (*)(); - -#endif // INFINIFRAME_TYPES_CALLBACKS_H diff --git a/src/InfiniFrame.NativeBridge/Native/Types/Dialog.h b/src/InfiniFrame.NativeBridge/Native/Types/Dialog.h index 690088813..eff083764 100644 --- a/src/InfiniFrame.NativeBridge/Native/Types/Dialog.h +++ b/src/InfiniFrame.NativeBridge/Native/Types/Dialog.h @@ -1,66 +1,8 @@ #pragma once -/** - * @file Dialog.h - * @brief Dialog-related types and enums - */ - -#ifndef INFINIFRAME_TYPES_DIALOG_H -#define INFINIFRAME_TYPES_DIALOG_H - // --------------------------------------------------------------------------------------------------------------------- -// Dialog Result +// Imports // --------------------------------------------------------------------------------------------------------------------- - -/** @brief Button pressed by the user to dismiss a message box */ -enum class DialogResult { - Cancel = -1, /// Dialog was cancelled (Escape key or window close) - Ok, /// User pressed OK - Yes, /// User pressed Yes - No, /// User pressed No - Abort, /// User pressed Abort - Retry, /// User pressed Retry - Ignore, /// User pressed Ignore -}; - -// --------------------------------------------------------------------------------------------------------------------- -// Dialog Buttons -// --------------------------------------------------------------------------------------------------------------------- - -/** @brief Button set to display in a message box */ -enum class DialogButtons { - Ok, /// Single OK button - OkCancel, /// OK and Cancel buttons - YesNo, /// Yes and No buttons - YesNoCancel, /// Yes, No, and Cancel buttons - RetryCancel, /// Retry and Cancel buttons - AbortRetryIgnore, /// Abort, Retry, and Ignore buttons -}; - -// --------------------------------------------------------------------------------------------------------------------- -// Dialog Icon -// --------------------------------------------------------------------------------------------------------------------- - -/** @brief Icon shown in a message box */ -enum class DialogIcon { - Info, - Warning, - Error, - Question, -}; - -// --------------------------------------------------------------------------------------------------------------------- -// Monitor -// --------------------------------------------------------------------------------------------------------------------- - -/** @brief Describes the geometry of a single display */ -struct Monitor { - /** @brief Pixel rectangle relative to the virtual desktop */ - struct MonitorRect { - int x, y; /// Top-left corner in virtual-desktop coordinates - int width, height; /// Dimensions in physical pixels - } monitor, /// Full monitor bounds (including taskbar) - work; /// Work area bounds (excluding taskbar and docked toolbars) - double scale; /// DPI scale factor (1.0 = 100%, 1.5 = 150%) -}; - -#endif // INFINIFRAME_TYPES_DIALOG_H +#include "DialogButtons.h" +#include "DialogIcon.h" +#include "DialogResult.h" +#include "Monitor.h" diff --git a/src/InfiniFrame.NativeBridge/Native/Types/DialogButtons.h b/src/InfiniFrame.NativeBridge/Native/Types/DialogButtons.h new file mode 100644 index 000000000..e2296092f --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Types/DialogButtons.h @@ -0,0 +1,17 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +enum class DialogButtons { + Ok, + OkCancel, + YesNo, + YesNoCancel, + RetryCancel, + AbortRetryIgnore, +}; + diff --git a/src/InfiniFrame.NativeBridge/Native/Types/DialogIcon.h b/src/InfiniFrame.NativeBridge/Native/Types/DialogIcon.h new file mode 100644 index 000000000..158607749 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Types/DialogIcon.h @@ -0,0 +1,15 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +enum class DialogIcon { + Info, + Warning, + Error, + Question, +}; + diff --git a/src/InfiniFrame.NativeBridge/Native/Types/DialogResult.h b/src/InfiniFrame.NativeBridge/Native/Types/DialogResult.h new file mode 100644 index 000000000..111b4f2ed --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Types/DialogResult.h @@ -0,0 +1,17 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +enum class DialogResult { + Cancel = -1, + Ok, + Yes, + No, + Abort, + Retry, + Ignore, +}; diff --git a/src/InfiniFrame.NativeBridge/Native/Types/Monitor.h b/src/InfiniFrame.NativeBridge/Native/Types/Monitor.h new file mode 100644 index 000000000..f407ab12b --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Types/Monitor.h @@ -0,0 +1,15 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +struct Monitor { + struct MonitorRect { + int x, y; + int width, height; + } monitor, work; + double scale; +}; diff --git a/src/InfiniFrame.NativeBridge/Native/Utils/Common.h b/src/InfiniFrame.NativeBridge/Native/Utils/Common.h index f01864e51..5e4cd6cd0 100644 --- a/src/InfiniFrame.NativeBridge/Native/Utils/Common.h +++ b/src/InfiniFrame.NativeBridge/Native/Utils/Common.h @@ -1,186 +1,9 @@ #pragma once -/** - * @file Common.h - * @brief Common utilities for cross-platform development - */ - -#ifndef INFINIFRAME_COMMON_H -#define INFINIFRAME_COMMON_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#endif - // --------------------------------------------------------------------------------------------------------------------- -// Constants +// Imports // --------------------------------------------------------------------------------------------------------------------- - -inline constexpr int MaxWindowDimension = 10000; -inline constexpr int MinWindowDimension = 50; -inline constexpr int DefaultWindowWidth = 800; -inline constexpr int DefaultWindowHeight = 600; - -// --------------------------------------------------------------------------------------------------------------------- -// Error Codes -// --------------------------------------------------------------------------------------------------------------------- - -enum class ErrorCode { - Success = 0, - InvalidArgument, - NotInitialized, - PlatformNotSupported, - WebViewError, - EncodingError, - MemoryError, - IoError, - NullPointer, - InterfaceNotAvailable, - PropertyAccessFailed, - WindowNotFound -}; - -// --------------------------------------------------------------------------------------------------------------------- -// Error Category -// --------------------------------------------------------------------------------------------------------------------- - -inline const std::error_category& errorCategory() noexcept { - struct InfiniFrameCategory : std::error_category { - const char*name() const noexcept override { - return "InfiniFrame"; - } - - std::string message(int ev) const override { - switch (static_cast(ev)) { - case ErrorCode::Success: - return "Success"; - case ErrorCode::InvalidArgument: - return "Invalid argument"; - case ErrorCode::NotInitialized: - return "Not initialized"; - case ErrorCode::PlatformNotSupported: - return "Platform not supported"; - case ErrorCode::WebViewError: - return "WebView error"; - case ErrorCode::EncodingError: - return "Encoding error"; - case ErrorCode::MemoryError: - return "Memory error"; - case ErrorCode::IoError: - return "I/O error"; - case ErrorCode::NullPointer: - return "Null pointer"; - case ErrorCode::InterfaceNotAvailable: - return "Interface not available"; - case ErrorCode::PropertyAccessFailed: - return "Property access failed"; - case ErrorCode::WindowNotFound: - return "Window not found"; - default: - return "Unknown error"; - } - } - }; - static const InfiniFrameCategory category; - return category; -} - -inline std::error_code make_error_code(ErrorCode e) noexcept { - return {static_cast(e), errorCategory()}; -} - -namespace std { - template <> - struct is_error_code_enum : true_type { - }; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Result Type -// --------------------------------------------------------------------------------------------------------------------- - -template -using Result = std::expected; - -// --------------------------------------------------------------------------------------------------------------------- -// RAII Wrappers (Windows) -// --------------------------------------------------------------------------------------------------------------------- - -#ifdef _WIN32 - -struct HBRUSHDeleter { - void operator()(void* h) const noexcept { - if (h) - DeleteObject(static_cast(h)); - } -}; - -struct HICONDeleter { - void operator()(void* h) const noexcept { - if (h) - DestroyIcon(static_cast(h)); - } -}; - -struct HDCDeleter { - void operator()(void* h) const noexcept { - if (h) - DeleteDC(static_cast(h)); - } -}; - -using UniqueHBRUSH = std::unique_ptr; -using UniqueHICON = std::unique_ptr; -using UniqueHDC = std::unique_ptr; - -#endif - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Functions -// --------------------------------------------------------------------------------------------------------------------- - -template -[[nodiscard]] constexpr T clampDimension(T value, T minVal = MinWindowDimension, T maxVal = MaxWindowDimension) { - return std::clamp(value, minVal, maxVal); -} - -#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; -} - -#elif __linux__ -inline char* AllocateStringCopy(const std::string& str) { - return g_strdup(str.c_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; -} - -#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; -} -#endif - -#endif // INFINIFRAME_COMMON_H +#include "Dimensions.h" +#include "ErrorCode.h" +#include "Result.h" +#include "StringCopy.h" +#include "WindowsHandles.h" diff --git a/src/InfiniFrame.NativeBridge/Native/Utils/Dimensions.h b/src/InfiniFrame.NativeBridge/Native/Utils/Dimensions.h new file mode 100644 index 000000000..c8d3b6fb4 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Utils/Dimensions.h @@ -0,0 +1,17 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +inline constexpr int MaxWindowDimension = 10000; +inline constexpr int MinWindowDimension = 50; +inline constexpr int DefaultWindowWidth = 800; +inline constexpr int DefaultWindowHeight = 600; + +template +[[nodiscard]] constexpr T clampDimension(T value, T minVal = MinWindowDimension, T maxVal = MaxWindowDimension) { + return std::clamp(value, minVal, maxVal); +} diff --git a/src/InfiniFrame.NativeBridge/Native/Utils/ErrorCode.h b/src/InfiniFrame.NativeBridge/Native/Utils/ErrorCode.h new file mode 100644 index 000000000..5cf6fbebe --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Utils/ErrorCode.h @@ -0,0 +1,70 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +enum class ErrorCode { + Success = 0, + InvalidArgument, + NotInitialized, + PlatformNotSupported, + WebViewError, + EncodingError, + MemoryError, + IoError, + NullPointer, + InterfaceNotAvailable, + PropertyAccessFailed, + WindowNotFound +}; + +inline const std::error_category& errorCategory() noexcept { + struct InfiniFrameCategory : std::error_category { + const char* name() const noexcept override { + return "InfiniFrame"; + } + + std::string message(int ev) const override { + switch (static_cast(ev)) { + case ErrorCode::Success: + return "Success"; + case ErrorCode::InvalidArgument: + return "Invalid argument"; + case ErrorCode::NotInitialized: + return "Not initialized"; + case ErrorCode::PlatformNotSupported: + return "Platform not supported"; + case ErrorCode::WebViewError: + return "WebView error"; + case ErrorCode::EncodingError: + return "Encoding error"; + case ErrorCode::MemoryError: + return "Memory error"; + case ErrorCode::IoError: + return "I/O error"; + case ErrorCode::NullPointer: + return "Null pointer"; + case ErrorCode::InterfaceNotAvailable: + return "Interface not available"; + case ErrorCode::PropertyAccessFailed: + return "Property access failed"; + case ErrorCode::WindowNotFound: + return "Window not found"; + default: + return "Unknown error"; + } + } + }; + static const InfiniFrameCategory category; + return category; +} + +inline std::error_code make_error_code(const ErrorCode e) noexcept { + return {static_cast(e), errorCategory()}; +} + +template <> struct std::is_error_code_enum : true_type {}; diff --git a/src/InfiniFrame.NativeBridge/Native/Utils/Event.h b/src/InfiniFrame.NativeBridge/Native/Utils/Event.h index 3fb427af7..54f9db76c 100644 --- a/src/InfiniFrame.NativeBridge/Native/Utils/Event.h +++ b/src/InfiniFrame.NativeBridge/Native/Utils/Event.h @@ -1,155 +1,140 @@ #pragma once -/** - * @file Event.h - * @brief Modern event handling system with thread safety - */ - -#ifndef INFINIFRAME_EVENT_H -#define INFINIFRAME_EVENT_H - +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- #include #include #include #include -#include - // --------------------------------------------------------------------------------------------------------------------- -// Event System +// Code // --------------------------------------------------------------------------------------------------------------------- - -template -class Event { +template class Event { public: - using Handler = std::function; - using Token = size_t; + using Handler = std::function; + using Token = size_t; - Event() = default; - ~Event() = default; + Event() = default; + ~Event() = default; - Event(const Event&) = delete; - Event& operator=(const Event&) = delete; - Event(Event&&) noexcept = default; - Event& operator=(Event&&) noexcept = default; + Event(const Event&) = delete; + Event& operator=(const Event&) = delete; + Event(Event&&) noexcept = default; + Event& operator=(Event&&) noexcept = default; - /** + /** * @brief Subscribe to event * @param handler Callback function to invoke when event is raised * @return Token for unsubscribing */ - [[nodiscard]] Token Subscribe(Handler handler) { - std::unique_lock lock(m_mutex); - const auto token = m_nextToken++; - m_handlers.emplace(token, std::move(handler)); - return token; - } - - /** + [[nodiscard]] Token Subscribe(Handler handler) { + std::unique_lock lock(m_mutex); + const auto token = m_nextToken++; + m_handlers.emplace(token, std::move(handler)); + return token; + } + + /** * @brief Unsubscribe from event * @param token Token returned from Subscribe */ - void Unsubscribe(Token token) { - std::unique_lock lock(m_mutex); - m_handlers.erase(token); - } + void Unsubscribe(Token token) { + std::unique_lock lock(m_mutex); + m_handlers.erase(token); + } - /** + /** * @brief Raise event (invoke all handlers) * @param args Arguments to pass to handlers */ - void Raise(Args... args) { - std::shared_lock lock(m_mutex); - for (const auto& [_, handler] : m_handlers) { - if (handler) { - handler(args...); - } + void Raise(Args... args) { + std::shared_lock lock(m_mutex); + for (const auto& [_, handler] : m_handlers) { + if (handler) { + handler(args...); } } + } - /** + /** * @brief Check if event has subscribers * @return true if at least one handler is subscribed */ - [[nodiscard]] bool HasSubscribers() const { - std::shared_lock lock(m_mutex); - return !m_handlers.empty(); - } + [[nodiscard]] bool HasSubscribers() const { + std::shared_lock lock(m_mutex); + return !m_handlers.empty(); + } - /** + /** * @brief Clear all subscribers */ - void Clear() { - std::unique_lock lock(m_mutex); - m_handlers.clear(); - } + void Clear() { + std::unique_lock lock(m_mutex); + m_handlers.clear(); + } private: - mutable std::shared_mutex m_mutex; - std::map m_handlers; - Token m_nextToken = 1; + mutable std::shared_mutex m_mutex; + std::map m_handlers; + Token m_nextToken = 1; }; -// --------------------------------------------------------------------------------------------------------------------- -// Event Subscription Guard -// --------------------------------------------------------------------------------------------------------------------- - -template -class EventSubscription { +template class EventSubscription { public: - using EventType = Event; - using Token = EventType::Token; + using EventType = Event; + using Token = EventType::Token; - EventSubscription() = default; + EventSubscription() = default; - EventSubscription(EventType& event, EventType::Handler handler) : - m_event(&event), m_token(event.Subscribe(std::move(handler))) { - } + EventSubscription(EventType& event, EventType::Handler handler) + : m_event(&event) + , m_token(event.Subscribe(std::move(handler))) {} - ~EventSubscription() { - Unsubscribe(); - } + ~EventSubscription() { + Unsubscribe(); + } + + EventSubscription(const EventSubscription&) = delete; + EventSubscription& operator=(const EventSubscription&) = delete; - EventSubscription(const EventSubscription&) = delete; - EventSubscription& operator=(const EventSubscription&) = delete; + EventSubscription(EventSubscription&& other) noexcept + : m_event(other.m_event) + , m_token(other.m_token) { + other.m_event = nullptr; + other.m_token = 0; + } - EventSubscription(EventSubscription&& other) noexcept : - m_event(other.m_event), m_token(other.m_token) { + EventSubscription& operator=(EventSubscription&& other) noexcept { + if (this != &other) { + Unsubscribe(); + m_event = other.m_event; + m_token = other.m_token; other.m_event = nullptr; other.m_token = 0; } + return *this; + } - EventSubscription& operator=(EventSubscription&& other) noexcept { - if (this != &other) { - Unsubscribe(); - m_event = other.m_event; - m_token = other.m_token; - other.m_event = nullptr; - other.m_token = 0; - } - return *this; - } - - /** + /** * @brief Manually unsubscribe from event */ - void Unsubscribe() { - if (m_event && m_token != 0) { - m_event->Unsubscribe(m_token); - m_event = nullptr; - m_token = 0; - } + void Unsubscribe() { + if (m_event && m_token != 0) { + m_event->Unsubscribe(m_token); + m_event = nullptr; + m_token = 0; } + } - /** + /** * @brief Check if subscription is active * @return true if still subscribed */ - [[nodiscard]] bool IsActive() const noexcept { - return m_event != nullptr && m_token != 0; - } + [[nodiscard]] bool IsActive() const noexcept { + return m_event != nullptr && m_token != 0; + } private: - EventType* m_event = nullptr; - Token m_token = 0; + EventType* m_event = nullptr; + Token m_token = 0; }; - -#endif // INFINIFRAME_EVENT_H diff --git a/src/InfiniFrame.NativeBridge/Native/Utils/ExportGuards.h b/src/InfiniFrame.NativeBridge/Native/Utils/ExportGuards.h new file mode 100644 index 000000000..537f25b7a --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Utils/ExportGuards.h @@ -0,0 +1,189 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#include "Public/InfiniFrame.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +enum class InteropStatus : int { + Success = 0, + InvalidArgument = 22, + OutParameterSetToInvalidNull = 2001, + OperationFailed = 14 +}; + +namespace infiniframe::exports { + namespace detail { + inline thread_local std::string g_lastErrorMessage; + inline thread_local auto g_lastStatus = InteropStatus::Success; + + inline void SetLastErrorCode(const InteropStatus status) noexcept { +#ifdef _WIN32 + SetLastError(static_cast(status)); +#else + errno = static_cast(status); +#endif + } + + inline void ClearLastErrorCode() noexcept { +#ifdef _WIN32 + SetLastError(0); +#else + errno = 0; +#endif + } + + inline void SetFailure(const InteropStatus status, std::string message) noexcept { + g_lastErrorMessage = std::move(message); + g_lastStatus = status; + SetLastErrorCode(status); + } + + inline void SetSuccess() noexcept { + g_lastErrorMessage.clear(); + g_lastStatus = InteropStatus::Success; + ClearLastErrorCode(); + } + + inline InteropStatus TranslateException(const std::exception& ex) noexcept { + if (dynamic_cast(&ex) != nullptr) { + SetFailure(InteropStatus::InvalidArgument, ex.what()); + return InteropStatus::InvalidArgument; + } + + SetFailure(InteropStatus::OperationFailed, ex.what()); + return InteropStatus::OperationFailed; + } + +#ifdef _WIN32 + inline AutoString AllocateErrorMessageString(const std::string& value) { + if (value.empty()) { + return nullptr; + } + + const int wideCount = + MultiByteToWideChar(CP_UTF8, 0, value.c_str(), static_cast(value.size()), nullptr, 0); + if (wideCount <= 0) { + return nullptr; + } + + auto* buffer = new wchar_t[wideCount + 1]; + const int converted = + MultiByteToWideChar(CP_UTF8, 0, value.c_str(), static_cast(value.size()), buffer, wideCount); + if (converted <= 0) { + delete[] buffer; + return nullptr; + } + + buffer[converted] = L'\0'; + return buffer; + } +#else + inline AutoString AllocateErrorMessageString(const std::string& value) { + if (value.empty()) { + return nullptr; + } + + return AllocateStringCopy(value); + } +#endif + } // namespace detail + + inline AutoString GetLastErrorMessageCopy() { + return detail::AllocateErrorMessageString(detail::g_lastErrorMessage); + } + + template void ResetOut(T* outValue, const T fallback = {}) noexcept { + if (outValue != nullptr) { + *outValue = fallback; + } + } + + template void ResetOut2(T* first, T* second, const T fallback = {}) noexcept { + ResetOut(first, fallback); + ResetOut(second, fallback); + } + + template + bool EnsureNotNull( + T* value, const char* argumentName, const InteropStatus status = InteropStatus::InvalidArgument + ) noexcept { + if (value != nullptr) { + return true; + } + + detail::SetFailure(status, std::string("Argument '") + argumentName + "' is null."); + return false; + } + + template InteropStatus RunExportStatus(Fn&& fn) noexcept { + try { + detail::SetSuccess(); + std::forward(fn)(); + if (detail::g_lastStatus != InteropStatus::Success) { + return detail::g_lastStatus; + } + detail::SetSuccess(); + return InteropStatus::Success; + } catch (const std::exception& ex) { + return detail::TranslateException(ex); + } catch (...) { + detail::SetFailure(InteropStatus::OperationFailed, "Unknown native exception."); + return InteropStatus::OperationFailed; + } + } + + template InteropStatus RunWindowExportStatus(InfiniFrameWindow* instance, Fn&& fn) noexcept { + return RunExportStatus([&] { + if (!EnsureNotNull(instance, "instance")) { + return; + } + + std::forward(fn)(instance); + }); + } + + template + T RunWindowReturnExport(InfiniFrameWindow* instance, T fallback, Fn&& fn) noexcept { + try { + if (!EnsureNotNull(instance, "instance")) { + return fallback; + } + + T value = std::forward(fn)(instance); + detail::SetSuccess(); + return value; + } catch (const std::exception& ex) { + detail::TranslateException(ex); + return fallback; + } catch (...) { + detail::SetFailure(InteropStatus::OperationFailed, "Unknown native exception."); + return fallback; + } + } + + template T RunReturnExport(T fallback, Fn&& fn) noexcept { + try { + T value = std::forward(fn)(); + detail::SetSuccess(); + return value; + } catch (const std::exception& ex) { + detail::TranslateException(ex); + return fallback; + } catch (...) { + detail::SetFailure(InteropStatus::OperationFailed, "Unknown native exception."); + return fallback; + } + } +} diff --git a/src/InfiniFrame.NativeBridge/Native/Utils/Result.h b/src/InfiniFrame.NativeBridge/Native/Utils/Result.h new file mode 100644 index 000000000..7ed7361b9 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Utils/Result.h @@ -0,0 +1,11 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include + +#include "Utils/ErrorCode.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +template using Result = std::expected; diff --git a/src/InfiniFrame.NativeBridge/Native/Utils/StringCopy.h b/src/InfiniFrame.NativeBridge/Native/Utils/StringCopy.h new file mode 100644 index 000000000..5a1355fec --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Utils/StringCopy.h @@ -0,0 +1,43 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include +#include + +#ifdef __linux__ +#include +#endif +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +#ifdef _WIN32 +inline wchar_t* AllocateStringCopy(const std::wstring& str) { + const size_t len = str.length(); + auto* copy = new wchar_t[len + 1]; + std::memcpy(copy, str.c_str(), (len + 1) * sizeof(wchar_t)); + return copy; +} + +#elif __linux__ +inline char* AllocateStringCopy(const std::string& str) { + return g_strdup(str.c_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; +} + +#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; +} +#endif diff --git a/src/InfiniFrame.NativeBridge/Native/Utils/WindowsHandles.h b/src/InfiniFrame.NativeBridge/Native/Utils/WindowsHandles.h new file mode 100644 index 000000000..9ed5445dd --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/Utils/WindowsHandles.h @@ -0,0 +1,38 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#ifdef _WIN32 +#include +#include +#endif +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +#ifdef _WIN32 +struct HBRUSHDeleter { + void operator()(void* h) const noexcept { + if (h) + DeleteObject(h); + } +}; + +struct HICONDeleter { + void operator()(void* h) const noexcept { + if (h) + DestroyIcon(static_cast(h)); + } +}; + +struct HDCDeleter { + void operator()(void* h) const noexcept { + if (h) + DeleteDC(static_cast(h)); + } +}; + +using UniqueHBRUSH = std::unique_ptr; +using UniqueHICON = std::unique_ptr; +using UniqueHDC = std::unique_ptr; + +#endif \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/native-build.ps1 b/src/InfiniFrame.NativeBridge/native-build.ps1 index 91fb8f8af..85af1774c 100644 --- a/src/InfiniFrame.NativeBridge/native-build.ps1 +++ b/src/InfiniFrame.NativeBridge/native-build.ps1 @@ -1,6 +1,7 @@ param( [string]$Configuration = "Debug", - [string]$Arch = "x64" + [string]$Arch = "x64", + [string]$EnableTestExports = "" ) $ErrorActionPreference = "Stop" @@ -27,10 +28,18 @@ else { "osx" } New-Item -ItemType Directory -Force -Path "$ArtifactsDir/$Platform/$Arch/$Configuration" | Out-Null +if ([string]::IsNullOrWhiteSpace($EnableTestExports)) { + $EnableTestExports = if ($Configuration -ieq "Debug") { "true" } else { "false" } +} + +$EnableTestExportsCMakeValue = if ($EnableTestExports -ieq "true") { "ON" } else { "OFF" } + # ----------------------------------------------------------------------------------------------------------------- # LOCK (blocking, CI-safe, race-free) # ----------------------------------------------------------------------------------------------------------------- $LockFile = Join-Path $ArtifactsDir ".build.lock" +$LockTimeoutSeconds = 600 +$LockDeadline = (Get-Date).AddSeconds($LockTimeoutSeconds) $LockStream = $null @@ -38,6 +47,10 @@ try { # Wait until lock becomes available (prevents crash) while ($true) { + if ((Get-Date) -ge $LockDeadline) { + throw "Timed out after $LockTimeoutSeconds seconds waiting for native build lock at '$LockFile'." + } + try { $LockStream = New-Object System.IO.FileStream( $LockFile, @@ -60,6 +73,7 @@ try { Write-Host "Configuration: $Configuration" Write-Host "Architecture : $Arch" Write-Host "Platform : $Platform" + Write-Host "Test Exports : $EnableTestExports" Write-Host "=========================================" # ----------------------------------------------------------------------------------------------------------------- @@ -70,9 +84,19 @@ try { if ($Platform -eq "osx") { if ($Arch -eq "arm64") { $CMakeArgs += "-DCMAKE_OSX_ARCHITECTURES=arm64" - } else { + } + elseif ($Arch -eq "x64") { $CMakeArgs += "-DCMAKE_OSX_ARCHITECTURES=x86_64" } + else { + throw "Unsupported macOS architecture '$Arch'. Expected 'x64' or 'arm64'." + } + } + + if ($Platform -eq "linux") { + if ($Arch -ne "x64" -and $Arch -ne "arm64") { + throw "Unsupported Linux architecture '$Arch'. Expected 'x64' or 'arm64'." + } } if ($Platform -eq "windows") { @@ -89,6 +113,8 @@ try { } } + $CMakeArgs += "-DINFINIFRAME_BUILD_TEST_EXPORTS=$EnableTestExportsCMakeValue" + cmake -B $BuildDir -S $NativeDir @CMakeArgs if ($LASTEXITCODE -ne 0) { throw "CMake configure failed with exit code $LASTEXITCODE." diff --git a/src/InfiniFrame.NativeBridge/native-clean.ps1 b/src/InfiniFrame.NativeBridge/native-clean.ps1 index 542315978..a503e7e97 100644 --- a/src/InfiniFrame.NativeBridge/native-clean.ps1 +++ b/src/InfiniFrame.NativeBridge/native-clean.ps1 @@ -4,5 +4,12 @@ Write-Host "Cleaning native build directories..." Remove-Item -Recurse -Force "$RootDir/build" -ErrorAction SilentlyContinue Remove-Item -Recurse -Force "$RootDir/artifacts" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force "$RootDir/Native/build" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force "$RootDir/Native/build-clang-tidy" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force "$RootDir/Native/cmake-build-debug-windows" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force "$RootDir/Native/cmake-build-debug-linux" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force "$RootDir/Native/cmake-build-release-windows" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force "$RootDir/Native/cmake-build-release-linux" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force "$RootDir/Native/packages" -ErrorAction SilentlyContinue Write-Host "Native clean complete." \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/native-format.ps1 b/src/InfiniFrame.NativeBridge/native-format.ps1 new file mode 100644 index 000000000..cb3a64da8 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/native-format.ps1 @@ -0,0 +1,10 @@ +Get-ChildItem -Recurse -File -Include *.cpp,*.cxx,*.cc,*.c,*.hpp,*.hh,*.hxx,*.h,*.ixx,*.mm,*.m | +Where-Object { + $_.FullName -notmatch '\\Dependencies\\' -and + $_.FullName -notmatch '\\packages\\' -and + $_.FullName -notmatch '\\build\\' +} | +ForEach-Object { + Write-Host "Formatting $($_.FullName)" + clang-format -i $_.FullName +} \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/native-tidy.ps1 b/src/InfiniFrame.NativeBridge/native-tidy.ps1 new file mode 100644 index 000000000..6d2e04f4c --- /dev/null +++ b/src/InfiniFrame.NativeBridge/native-tidy.ps1 @@ -0,0 +1,141 @@ +param( + [string]$BuildDirectoryName = "build-clang-tidy", + [switch]$ApplyFixes, + [switch]$FixErrors +) + +$ErrorActionPreference = "Stop" + +function Invoke-ExternalCommand { + param( + [Parameter(Mandatory = $true)][string]$FilePath, + [string[]]$Arguments = @() + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($Arguments -join ' ')" + } +} + +function Get-VsInstallationPath { + $vsWhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" + if (-not (Test-Path $vsWhere)) { + return $null + } + + $path = & $vsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($path)) { + return $null + } + + return $path.Trim() +} + +function Import-VsDevEnvironment { + param([Parameter(Mandatory = $true)][string]$VsDevCmdPath) + + $envDump = & cmd.exe /s /c "`"$VsDevCmdPath`" -arch=x64 -host_arch=x64 >nul && set" + if ($LASTEXITCODE -ne 0) { + throw "Failed to initialize Visual Studio developer environment using '$VsDevCmdPath'." + } + + foreach ($line in $envDump) { + $separatorIndex = $line.IndexOf("=") + if ($separatorIndex -lt 1) { + continue + } + + $name = $line.Substring(0, $separatorIndex) + $value = $line.Substring($separatorIndex + 1) + Set-Item -Path "env:$name" -Value $value + } +} + +$NativeRoot = Join-Path $PSScriptRoot "Native" +$BuildDirectory = Join-Path $NativeRoot $BuildDirectoryName +$DependenciesRoot = Join-Path $NativeRoot "Dependencies" + +Push-Location $NativeRoot + +try { + if ($FixErrors -and -not $ApplyFixes) { + $ApplyFixes = $true + } + + $clangTidy = Get-Command clang-tidy -ErrorAction SilentlyContinue + if (-not $clangTidy) { + throw "clang-tidy was not found on PATH." + } + + $vsInstallPath = Get-VsInstallationPath + if (-not $vsInstallPath) { + throw "Could not locate a Visual Studio installation with C++ tools." + } + + $vsDevCmd = Join-Path $vsInstallPath "Common7\Tools\VsDevCmd.bat" + if (-not (Test-Path $vsDevCmd)) { + throw "VsDevCmd.bat not found at '$vsDevCmd'." + } + + Write-Host "Initializing Visual Studio developer environment..." + Import-VsDevEnvironment -VsDevCmdPath $vsDevCmd + + $ninja = Get-Command ninja -ErrorAction SilentlyContinue + if (-not $ninja) { + throw "ninja was not found on PATH after VsDevCmd initialization." + } + + Write-Host "Generating compile_commands.json in '$BuildDirectoryName'..." + Invoke-ExternalCommand -FilePath "cmake" -Arguments @( + "-S", ".", + "-B", $BuildDirectory, + "--fresh", + "-G", "Ninja", + "-DCMAKE_MAKE_PROGRAM=$($ninja.Source)", + "-DCMAKE_BUILD_TYPE=Debug", + "-DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON", + "-DCMAKE_CXX_SCAN_FOR_MODULES=OFF", + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" + ) + + $compileCommandsPath = Join-Path $BuildDirectory "compile_commands.json" + if (-not (Test-Path $compileCommandsPath)) { + throw "compile_commands.json was not generated at '$compileCommandsPath'." + } + + Write-Host "Running clang-tidy..." + + $sourceFiles = Get-ChildItem . -Recurse -File -Include *.cpp,*.cc,*.cxx,*.c,*.mm | + Where-Object { + $_.FullName -notmatch '\\(build($|[-_][^\\]+)?|out|bin|obj|vcpkg|Dependencies|packages)\\' + } + + foreach ($sourceFile in $sourceFiles) { + Write-Host "Running clang-tidy on $($sourceFile.FullName)" + + $tidyArgs = @( + $sourceFile.FullName, + "-p", $BuildDirectory, + "--header-filter=^$", + "--extra-arg=/external:I$DependenciesRoot", + "--extra-arg=/external:W0", + "--extra-arg=-Wno-c++11-narrowing" + ) + + if ($ApplyFixes) { + $tidyArgs += "--fix" + } + + if ($ApplyFixes -and $FixErrors) { + $tidyArgs += "--fix-errors" + } + + Invoke-ExternalCommand -FilePath $clangTidy.Source -Arguments $tidyArgs + } + + Write-Host "clang-tidy complete." +} +finally { + Pop-Location +} diff --git a/src/InfiniFrame.Shared/InfiniFrame.Shared.csproj b/src/InfiniFrame.Shared/InfiniFrame.Shared.csproj index ad0d0eddc..a485673a2 100644 --- a/src/InfiniFrame.Shared/InfiniFrame.Shared.csproj +++ b/src/InfiniFrame.Shared/InfiniFrame.Shared.csproj @@ -15,7 +15,9 @@ - + + true + diff --git a/src/InfiniFrame.Shared/Utilities/InvokeUtility.cs b/src/InfiniFrame.Shared/Utilities/InvokeUtility.cs index 7c6a3fcd1..bb3fb494e 100644 --- a/src/InfiniFrame.Shared/Utilities/InvokeUtility.cs +++ b/src/InfiniFrame.Shared/Utilities/InvokeUtility.cs @@ -53,5 +53,22 @@ public static T InvokeAndReturn(IInfiniFrameWindow window, FuncWithOut cal return value!; } + public static T InvokeAndReturn(IInfiniFrameWindow window, FuncWithOutResult callback, Action? validateResult = null) { + T? value = default; + TResult? result = default; + // ReSharper disable once RedundantAssignment + bool completed = false; + window.Invoke(() => { + result = callback(window.InstanceHandle, out value); + completed = true; + }); + Debug.Assert(completed, "Invoke must be synchronous — callback did not complete before Invoke returned."); + if (validateResult is not null && result is not null) { + validateResult(result); + } + return value!; + } + internal delegate void FuncWithOut(IntPtr handle, out T value); + internal delegate TResult FuncWithOutResult(IntPtr handle, out T value); } diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index b8529a469..9d8714662 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -50,9 +50,21 @@ public sealed class InfiniFrameWindow : IInfiniFrameWindow { /// /// The delegate encapsulating a method / action to be executed in the UI thread. public void Invoke(Action workItem) { + static void RunWithNativeStatusCheck(Action action) { + Marshal.SetLastPInvokeError(0); + action(); + InfiniFrameNative.EnsureSucceeded(InfiniFrameNativeInteropStatus.Success, "Invoke"); + } + // If we're already on the UI thread, no need to dispatch - if (Environment.CurrentManagedThreadId == ManagedThreadId) workItem(); - else InfiniFrameNative.Invoke(InstanceHandle, workItem.Invoke); + if (Environment.CurrentManagedThreadId == ManagedThreadId) { + RunWithNativeStatusCheck(workItem); + } + else { + InfiniFrameNative.EnsureSucceeded( + InfiniFrameNative.Invoke(InstanceHandle, () => RunWithNativeStatusCheck(workItem)), + nameof(InfiniFrameNative.Invoke)); + } } /// @@ -70,8 +82,7 @@ public void WaitForClose() { } catch (Exception ex) when (ExceptionsUtility.IsNonFatalException(ex)) { int lastError = 0; - if (OperatingSystem.IsWindows()) - lastError = Marshal.GetLastWin32Error(); + if (OperatingSystem.IsWindows()) lastError = Marshal.GetLastWin32Error(); Logger.LogError(ex, "Error #{LastErrorCode} while running message loop", lastError); throw new ApplicationException($"Native code exception. Error # {lastError} See inner exception for details.", ex); @@ -274,7 +285,9 @@ public void SendNotification(string title, string body) { string[] nativeFilters = GetNativeFilters(filters); Invoke(() => { - IntPtr ptrResult = InfiniFrameNative.ShowSaveFile(InstanceHandle, title, defaultPath, nativeFilters, filters.Length, null); + InfiniFrameNative.EnsureSucceeded( + InfiniFrameNative.ShowSaveFile(InstanceHandle, title, defaultPath, nativeFilters, filters.Length, null, out IntPtr ptrResult), + nameof(InfiniFrameNative.ShowSaveFile)); if (ptrResult == IntPtr.Zero) return; try { @@ -320,7 +333,11 @@ public void SendNotification(string title, string body) { /// public InfiniFrameDialogResult ShowMessage(string title, string? text, InfiniFrameDialogButtons buttons = InfiniFrameDialogButtons.Ok, InfiniFrameDialogIcon icon = InfiniFrameDialogIcon.Info) { var result = InfiniFrameDialogResult.Cancel; - Invoke(() => result = InfiniFrameNative.ShowMessage(InstanceHandle, title, text ?? string.Empty, buttons, icon)); + Invoke(() => { + InfiniFrameNative.EnsureSucceeded( + InfiniFrameNative.ShowMessage(InstanceHandle, title, text ?? string.Empty, buttons, icon, out result), + nameof(InfiniFrameNative.ShowMessage)); + }); return result; } @@ -357,11 +374,20 @@ public void Initialize() { // All C++ exceptions will bubble up to here. try { if (OperatingSystem.IsWindows()) - Invoke(() => InfiniFrameNative.RegisterWin32(NativeType)); + Invoke(() => { + InfiniFrameNative.RegisterWin32(NativeType); + }); else if (OperatingSystem.IsMacOS()) - Invoke(InfiniFrameNative.RegisterMac); - - Invoke(() => InstanceHandle = InfiniFrameNative.Constructor(in startupParameters)); + Invoke(() => { + InfiniFrameNative.RegisterMac(); + }); + + Invoke(() => { + InfiniFrameNative.EnsureSucceeded( + InfiniFrameNative.Constructor(in startupParameters, out IntPtr instanceHandle), + nameof(InfiniFrameNative.Constructor)); + InstanceHandle = instanceHandle; + }); } catch (Exception ex) when (ExceptionsUtility.IsNonFatalException(ex)) { int lastError = 0; @@ -396,7 +422,10 @@ public void Initialize() { string[] nativeFilters = GetNativeFilters(filters, foldersOnly); Invoke(() => { - IntPtr ptrResults = foldersOnly ? InfiniFrameNative.ShowOpenFolder(InstanceHandle, title, defaultPath, multiSelect, out int resultCount) : InfiniFrameNative.ShowOpenFile(InstanceHandle, title, defaultPath, multiSelect, nativeFilters, nativeFilters.Length, out resultCount); + InfiniFrameNativeInteropStatus status = foldersOnly + ? InfiniFrameNative.ShowOpenFolder(InstanceHandle, title, defaultPath, multiSelect, out int resultCount, out IntPtr ptrResults) + : InfiniFrameNative.ShowOpenFile(InstanceHandle, title, defaultPath, multiSelect, nativeFilters, nativeFilters.Length, out resultCount, out ptrResults); + InfiniFrameNative.EnsureSucceeded(status, foldersOnly ? nameof(InfiniFrameNative.ShowOpenFolder) : nameof(InfiniFrameNative.ShowOpenFile)); if (resultCount == 0 || ptrResults == IntPtr.Zero) return; @@ -449,7 +478,10 @@ private static string[] GetNativeFilters((string Name, string[] Extensions)[] fi /// Thrown when accessed from a non-Windows platform. [DebuggerBrowsable(DebuggerBrowsableState.Never)] public IntPtr WindowHandle => OperatingSystem.IsWindows() - ? InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetWindowHandlerWin32) + ? InvokeUtility.InvokeAndReturn( + this, + InfiniFrameNative.GetWindowHandlerWin32, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetWindowHandlerWin32))) : IntPtr.Zero; /// @@ -484,7 +516,10 @@ private static string[] GetNativeFilters((string Name, string[] Extensions)[] fi /// An ApplicationException is thrown if the window hasn't been initialized yet. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public uint ScreenDpi => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetScreenDpi); + public uint ScreenDpi => InvokeUtility.InvokeAndReturn( + this, + InfiniFrameNative.GetScreenDpi, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetScreenDpi))); /// /// Gets a unique GUID to identify the native window. @@ -516,48 +551,51 @@ private static string[] GetNativeFilters((string Name, string[] Extensions)[] fi [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool Transparent => OperatingSystem.IsWindows() ? Configuration.StartupParameters.Transparent// on windows it can only be set at startup - : InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetTransparentEnabled); + : InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetTransparentEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetTransparentEnabled))); /// /// When true, the user can access the browser control's context menu. /// By default, this is set to true. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool ContextMenuEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetContextMenuEnabled); + public bool ContextMenuEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetContextMenuEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetContextMenuEnabled))); /// /// When true, the user can access the browser control's developer tools. /// By default, this is set to true. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool DevToolsEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetDevToolsEnabled); + public bool DevToolsEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetDevToolsEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetDevToolsEnabled))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool MediaAutoplayEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMediaAutoplayEnabled); + public bool MediaAutoplayEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMediaAutoplayEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMediaAutoplayEnabled))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public string? UserAgent => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetUserAgent); + public string? UserAgent => InvokeUtility.InvokeAndReturn( + this, + InfiniFrameNative.GetUserAgent, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetUserAgent))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool FileSystemAccessEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetFileSystemAccessEnabled); + public bool FileSystemAccessEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetFileSystemAccessEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetFileSystemAccessEnabled))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool WebSecurityEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetWebSecurityEnabled); + public bool WebSecurityEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetWebSecurityEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetWebSecurityEnabled))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool JavascriptClipboardAccessEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetJavascriptClipboardAccessEnabled); + public bool JavascriptClipboardAccessEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetJavascriptClipboardAccessEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetJavascriptClipboardAccessEnabled))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool MediaStreamEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMediaStreamEnabled); + public bool MediaStreamEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMediaStreamEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMediaStreamEnabled))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool SmoothScrollingEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetSmoothScrollingEnabled); + public bool SmoothScrollingEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetSmoothScrollingEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetSmoothScrollingEnabled))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool IgnoreCertificateErrorsEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetIgnoreCertificateErrorsEnabled); + public bool IgnoreCertificateErrorsEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetIgnoreCertificateErrorsEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetIgnoreCertificateErrorsEnabled))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool NotificationsEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetNotificationsEnabled); + public bool NotificationsEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetNotificationsEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetNotificationsEnabled))); /// /// This property returns or sets the fullscreen status of the window. @@ -565,34 +603,47 @@ private static string[] GetNativeFilters((string Name, string[] Extensions)[] fi /// By default, this is set to false. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool FullScreen => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetFullScreen); + public bool FullScreen => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetFullScreen, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetFullScreen))); /// /// Gets whether the native browser control grants all requests for access to local resources /// such as the user's camera and microphone. By default, this is set to true. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool GrantBrowserPermissions => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetGrantBrowserPermissions); + public bool GrantBrowserPermissions => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetGrantBrowserPermissions, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetGrantBrowserPermissions))); /// /// Gets the Height property of the native window in pixels. /// The default value is 0. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int Height => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetHeight); + public int Height => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out int value) => InfiniFrameNative.GetSize(handle, out _, out value), + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetSize))); /// /// Gets the icon file for the native window title bar. /// The file must be located on the local machine and cannot be a URL. The default is none. /// - public string IconFilePath => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetIconFileName); + public string IconFilePath => InvokeUtility.InvokeAndReturn( + this, + InfiniFrameNative.GetIconFileName, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetIconFileName))); /// /// Gets the native window Left (X) and Top coordinates (Y) in pixels. /// Default is 0,0 that means the window will be aligned to the top-left edge of the screen. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public Point Location => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetPosition); + public Point Location => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out Point value) => { + InfiniFrameNativeInteropStatus status = InfiniFrameNative.GetPosition(handle, out int left, out int top); + value = new Point(left, top); + return status; + }, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetPosition))); /// /// Gets the native window Left (X) coordinate in pixels. @@ -600,76 +651,112 @@ private static string[] GetNativeFilters((string Name, string[] Extensions)[] fi /// The default value is 0, which means the window will be aligned to the left edge of the screen. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int Left => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetLeft); + public int Left => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out int value) => InfiniFrameNative.GetPosition(handle, out value, out _), + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetPosition))); /// /// Gets whether the native window is maximized. /// Default is false. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool Maximized => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMaximized); + public bool Maximized => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMaximized, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMaximized))); /// /// Gets whether the native window is currently within focus /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool Focused => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetFocused); + public bool Focused => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetFocused, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetFocused))); /// /// Gets the maximum size of the native window in pixels. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public Size MaxSize => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMaxSize); + public Size MaxSize => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out Size value) => { + InfiniFrameNativeInteropStatus status = InfiniFrameNative.GetMaxSize(handle, out int width, out int height); + value = new Size(width, height); + return status; + }, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMaxSize))); /// /// Gets the native window maximum height in pixels. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int MaxHeight => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMaxHeight); + public int MaxHeight => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out int value) => InfiniFrameNative.GetMaxSize(handle, out _, out value), + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMaxSize))); /// /// Gets the native window maximum width in pixels. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int MaxWidth => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMaxWidth); + public int MaxWidth => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out int value) => InfiniFrameNative.GetMaxSize(handle, out value, out _), + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMaxSize))); /// /// Gets whether the native window is minimized (hidden). /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool Minimized => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMinimized); + public bool Minimized => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMinimized, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMinimized))); /// /// Gets the minimum size of the native window in pixels. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public Size MinSize => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMinSize); + public Size MinSize => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out Size value) => { + InfiniFrameNativeInteropStatus status = InfiniFrameNative.GetMinSize(handle, out int width, out int height); + value = new Size(width, height); + return status; + }, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMinSize))); /// /// Gets the native window minimum height in pixels. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int MinHeight => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMinHeight); + public int MinHeight => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out int value) => InfiniFrameNative.GetMinSize(handle, out _, out value), + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMinSize))); /// /// Gets the native window minimum width in pixels. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int MinWidth => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetMinWidth); + public int MinWidth => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out int value) => InfiniFrameNative.GetMinSize(handle, out value, out _), + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetMinSize))); /// /// Gets whether the user can resize the native window. /// Default is true. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool Resizable => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetResizable); + public bool Resizable => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetResizable, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetResizable))); /// /// Gets the native window Size. This represents the width and the height of the window in pixels. /// The default Size is 0,0. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public Size Size => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetSize); + public Size Size => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out Size value) => { + InfiniFrameNativeInteropStatus status = InfiniFrameNative.GetSize(handle, out int width, out int height); + value = new Size(width, height); + return status; + }, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetSize))); /// /// Gets platform-specific initialization parameters for the native browser control on startup. @@ -745,28 +832,37 @@ private static string[] GetNativeFilters((string Name, string[] Extensions)[] fi /// The default is "InfiniFrame". /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public string? Title => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetTitle); + public string? Title => InvokeUtility.InvokeAndReturn( + this, + InfiniFrameNative.GetTitle, + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetTitle))); /// /// Gets the native window Top (Y) coordinate in pixels. /// Default is 0. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int Top => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetTop); + public int Top => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out int value) => InfiniFrameNative.GetPosition(handle, out _, out value), + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetPosition))); /// /// Gets whether the native window is always at the top of the z-order. /// Default is false. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool TopMost => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetTopmost); + public bool TopMost => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetTopmost, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetTopmost))); /// /// Gets the native window width in pixels. /// Default is 0. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int Width => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetWidth); + public int Width => InvokeUtility.InvokeAndReturn( + this, + (IntPtr handle, out int value) => InfiniFrameNative.GetSize(handle, out value, out _), + s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetSize))); /// /// Gets the native browser control . @@ -774,9 +870,9 @@ private static string[] GetNativeFilters((string Name, string[] Extensions)[] fi /// /// 100 = 100%, 50 = 50% [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int Zoom => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetZoom); + public int Zoom => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetZoom, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetZoom))); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public bool ZoomEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetZoomEnabled); + public bool ZoomEnabled => InvokeUtility.InvokeAndReturn(this, InfiniFrameNative.GetZoomEnabled, s => InfiniFrameNative.EnsureSucceeded(s, nameof(InfiniFrameNative.GetZoomEnabled))); #endregion } diff --git a/src/InfiniFrame/Window/InfiniFrameWindowExtensions.cs b/src/InfiniFrame/Window/InfiniFrameWindowExtensions.cs index 80a4fa255..65ebf7e3b 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindowExtensions.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindowExtensions.cs @@ -756,8 +756,20 @@ public static T SetTitle(this T window, string? title) where T : class, IInfi window.Logger.LogDebug(".SetTitle({Title})", title); window.Invoke(() => { - IntPtr ptr = InfiniFrameNative.GetTitle(window.InstanceHandle); - string? oldTitle = InfiniFrameNative.PtrToNativeString(ptr); + InfiniFrameNative.EnsureSucceeded( + InfiniFrameNative.GetTitle(window.InstanceHandle, out IntPtr ptr), + nameof(InfiniFrameNative.GetTitle)); + + string? oldTitle; + try { + oldTitle = InfiniFrameNative.PtrToNativeString(ptr); + } + finally { + if (ptr != IntPtr.Zero) { + InfiniFrameNative.FreeString(ptr); + } + } + if (title == oldTitle) return; InfiniFrameNative.SetTitle( diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index a0a3273a0..5b0752b19 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -3,39 +3,13 @@ net8.0;net9.0;net10.0 14.0 - + enable enable true false - false $(MSBuildProjectDirectory)\bin\$(Configuration)\$(TargetFramework)\ - $(Platform) - x64 - $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows))) - $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux))) - $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX))) - - x64 - arm64 - x64 - - build/$(CMakeArch)/$(Configuration) - windows - linux - osx - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\')) - $(SolutionDir) - $(RepoRootDir) - $(ResolvedSolutionDir)src/InfiniFrame.NativeBridge/artifacts/native - $(ResolvedSolutionDir)artifacts/native - $(NativeBridgeArtifactsRoot) - $(LegacyNativeArtifactsRoot) - $(NativeArtifactsRoot)/$(CMakeOSDir)/$(CMakeArch)/$(Configuration) - - $(DefineConstants);WINDOWS diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets deleted file mode 100644 index 3b2361f18..000000000 --- a/tests/Directory.Build.targets +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/tests/InfiniFrameTests/InfiniFrameNativeParameterTests.cs b/tests/InfiniFrameTests/InfiniFrameNativeParameterTests.cs index 049399028..4d7143435 100644 --- a/tests/InfiniFrameTests/InfiniFrameNativeParameterTests.cs +++ b/tests/InfiniFrameTests/InfiniFrameNativeParameterTests.cs @@ -11,6 +11,41 @@ namespace InfiniFrameTests; // --------------------------------------------------------------------------------------------------------------------- public class InfiniFrameNativeParameterTests { + [Test] + public async Task NativeExport_InvalidArgument_SetsDeterministicLastErrorAndMessage(CancellationToken ct = default) { + // Act + InfiniFrameNative.FreeString(IntPtr.Zero); + int lastError = Marshal.GetLastPInvokeError(); + string? message = InfiniFrameNative.GetLastErrorMessage(); + + // Assert + await Assert.That(lastError).IsEqualTo(22); + await Assert.That(message).IsNotNull(); + await Assert.That(message!).Contains("value"); + } + + [Test] + public async Task NativeExport_Success_ClearsLastError(CancellationToken ct = default) { + IntPtr[] customSchemeNames = new IntPtr[16]; + IntPtr newParametersPtr = IntPtr.Zero; + + try { + var parameters = new InfiniFrameNativeParameters { + StartUrl = "https://example.org", + CustomSchemeNames = customSchemeNames, + Size = Marshal.SizeOf() + }; + + newParametersPtr = InfiniFrameNativeTesting.NativeParametersReturnAsIsPtr(ref parameters); + + int lastError = Marshal.GetLastPInvokeError(); + await Assert.That(lastError).IsEqualTo(0); + } + finally { + InfiniFrameNativeTesting.FreeInitParams(newParametersPtr); + } + } + // This test should onl fails if the InfiniFrameNativeParameterTests C# struct is wrongly defined // and has parameters in the wrong order, compared to the struct on the c++ side. [Test]