fix(windows): restore Windows 7 startup compatibility
This commit is contained in:
parent
4aa36cb893
commit
e82c1aedf0
|
|
@ -0,0 +1,56 @@
|
|||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$RuntimeDirectory = (Join-Path $PSScriptRoot "..\..\src-tauri\webview2-fixed-runtime"),
|
||||
[string]$LoaderPath = (Join-Path ([System.IO.Path]::GetTempPath()) "dbx-win7-webview2-loader-probe\WebView2Loader.dll"),
|
||||
[string]$ExpectedVersion = "109.0.1518.78"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$runtimeDirectory = (Resolve-Path -LiteralPath $RuntimeDirectory).Path
|
||||
$runtimeExecutable = Join-Path $runtimeDirectory "msedgewebview2.exe"
|
||||
if (!(Test-Path -LiteralPath $runtimeExecutable -PathType Leaf)) {
|
||||
throw "WebView2 fixed runtime executable does not exist: $runtimeExecutable"
|
||||
}
|
||||
|
||||
$loaderPath = (Resolve-Path -LiteralPath $LoaderPath).Path
|
||||
$escapedLoaderPath = $loaderPath.Replace('"', '""')
|
||||
$source = @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class DbxWebView2LoaderProbe
|
||||
{
|
||||
[DllImport(@"$escapedLoaderPath", CharSet = CharSet.Unicode, ExactSpelling = true)]
|
||||
public static extern int GetAvailableCoreWebView2BrowserVersionString(
|
||||
string browserExecutableFolder,
|
||||
out IntPtr versionInfo);
|
||||
}
|
||||
"@
|
||||
|
||||
Add-Type -TypeDefinition $source -Language CSharp
|
||||
$versionPointer = [IntPtr]::Zero
|
||||
$result = [DbxWebView2LoaderProbe]::GetAvailableCoreWebView2BrowserVersionString(
|
||||
$runtimeDirectory,
|
||||
[ref]$versionPointer
|
||||
)
|
||||
if ($result -ne 0) {
|
||||
throw "WebView2 loader failed to recognize fixed runtime at $runtimeDirectory (HRESULT 0x$($result.ToString('X8')))."
|
||||
}
|
||||
if ($versionPointer -eq [IntPtr]::Zero) {
|
||||
throw "WebView2 loader returned an empty version pointer for $runtimeDirectory."
|
||||
}
|
||||
|
||||
try {
|
||||
$version = [Runtime.InteropServices.Marshal]::PtrToStringUni($versionPointer)
|
||||
}
|
||||
finally {
|
||||
[Runtime.InteropServices.Marshal]::FreeCoTaskMem($versionPointer)
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($version) -or !$version.StartsWith($ExpectedVersion)) {
|
||||
throw "Expected WebView2 fixed runtime $ExpectedVersion, detected '$version'."
|
||||
}
|
||||
|
||||
Write-Host "WebView2 fixed runtime probe passed: loader=$loaderPath runtime=$runtimeDirectory version=$version"
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InstallerPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$installerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
|
||||
$installDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "dbx-win7-installer-audit"
|
||||
if (Test-Path -LiteralPath $installDirectory) {
|
||||
Remove-Item -LiteralPath $installDirectory -Recurse -Force
|
||||
}
|
||||
|
||||
$installer = Start-Process -FilePath $installerPath -ArgumentList @("/S", "/D=$installDirectory") -Wait -PassThru
|
||||
if ($installer.ExitCode -ne 0) {
|
||||
throw "Windows 7 test installer failed with exit code $($installer.ExitCode)."
|
||||
}
|
||||
|
||||
$expectedFiles = @(
|
||||
(Join-Path $installDirectory "dbx.exe"),
|
||||
(Join-Path $installDirectory "webview2-fixed-runtime\msedgewebview2.exe"),
|
||||
(Join-Path $installDirectory "uninstall.exe")
|
||||
)
|
||||
foreach ($path in $expectedFiles) {
|
||||
if (!(Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
throw "Windows 7 test installer omitted required file: $path"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Windows 7 installer content audit passed: $installerPath"
|
||||
|
||||
$uninstallerPath = Join-Path $installDirectory "uninstall.exe"
|
||||
$uninstaller = Start-Process -FilePath $uninstallerPath -ArgumentList @("/S", "_?=$installDirectory") -Wait -PassThru
|
||||
if ($uninstaller.ExitCode -ne 0) {
|
||||
Write-Warning "Windows 7 test uninstaller returned exit code $($uninstaller.ExitCode)."
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BinaryPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (!(Test-Path -LiteralPath $BinaryPath -PathType Leaf)) {
|
||||
throw "Windows 7 PE audit target does not exist: $BinaryPath"
|
||||
}
|
||||
|
||||
$dumpbinCommand = Get-Command dumpbin.exe -ErrorAction SilentlyContinue
|
||||
$dumpbinPath = if ($null -ne $dumpbinCommand) { $dumpbinCommand.Source } else { $null }
|
||||
if ($null -eq $dumpbinPath) {
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path -LiteralPath $vswhere) {
|
||||
$visualStudio = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
|
||||
if ($visualStudio) {
|
||||
$dumpbin = Get-ChildItem (Join-Path $visualStudio "VC\Tools\MSVC") -Filter dumpbin.exe -Recurse |
|
||||
Where-Object { $_.FullName -match '\\bin\\Hostx64\\x64\\dumpbin\.exe$' } |
|
||||
Sort-Object FullName -Descending |
|
||||
Select-Object -First 1
|
||||
if ($null -ne $dumpbin) {
|
||||
$dumpbinPath = $dumpbin.FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $dumpbinPath) {
|
||||
throw "Unable to find dumpbin.exe for the Windows 7 PE compatibility audit."
|
||||
}
|
||||
|
||||
$imports = (& $dumpbinPath /nologo /imports $BinaryPath 2>&1 | Out-String)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dumpbin failed while auditing ${BinaryPath}:`n$imports"
|
||||
}
|
||||
|
||||
$forbiddenImports = [ordered]@{
|
||||
"combase.dll" = "COMBASE is only available starting with Windows 8; use OLE32 imports."
|
||||
"api-ms-win-core-winrt-" = "WinRT API sets are unavailable on Windows 7."
|
||||
"CoIncrementMTAUsage" = "CoIncrementMTAUsage is unavailable on Windows 7."
|
||||
"EventSetInformation" = "EventSetInformation is unavailable on Windows 7. Use the legacy WebView2 loader."
|
||||
"GetSystemTimePreciseAsFileTime" = "GetSystemTimePreciseAsFileTime is unavailable on Windows 7."
|
||||
"GetDpiForWindow" = "GetDpiForWindow is unavailable on Windows 7."
|
||||
"GetSystemMetricsForDpi" = "GetSystemMetricsForDpi is unavailable on Windows 7."
|
||||
"SetThreadDpiAwarenessContext" = "SetThreadDpiAwarenessContext is unavailable on Windows 7."
|
||||
"VCRUNTIME140.dll" = "The Windows 7 package must not require a separately installed VC++ Runtime."
|
||||
"VCRUNTIME140_1.dll" = "The Windows 7 package must not require a separately installed VC++ Runtime."
|
||||
"MSVCP140.dll" = "The Windows 7 package must not require a separately installed VC++ Runtime."
|
||||
"ucrtbase.dll" = "The Windows 7 package must link the Universal CRT statically."
|
||||
"api-ms-win-crt-" = "The Windows 7 package must not require separately installed Universal CRT API sets."
|
||||
}
|
||||
|
||||
$violations = @()
|
||||
foreach ($entry in $forbiddenImports.GetEnumerator()) {
|
||||
if ($imports -match [regex]::Escape($entry.Key)) {
|
||||
$violations += "$($entry.Key): $($entry.Value)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($violations.Count -gt 0) {
|
||||
$summary = $violations -join "`n"
|
||||
Write-Host "Full PE import table for diagnosis:"
|
||||
Write-Host $imports
|
||||
throw "Windows 7 incompatible PE imports detected in ${BinaryPath}:`n$summary"
|
||||
}
|
||||
|
||||
Write-Host "Windows 7 PE import audit passed: $BinaryPath"
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Newer WebView2 static loaders import EventSetInformation, which does not exist on Windows 7.
|
||||
# The loader entry points are stable, so the Win7 bundle uses the last verified compatible SDK loader.
|
||||
$sdkVersion = "1.0.1054.31"
|
||||
$sdkPackageSha256 = "0afe683aa3d143a5f6330db1ce833c69278b38fe5e1eadec52f26910ad26e22f"
|
||||
$loaderSha256 = "76314119685bbf4c2b2423a44e81b57beadc914c943d0e772fd6bc78c8e6b0e8"
|
||||
$webView2ComSysVersion = "0.38.2"
|
||||
$upstreamLoaderSha256 = "0659b741bde6348d4c4a6ec4ceb9af50e3d0048ed9cd3c8659bccbb61fde55ee"
|
||||
|
||||
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot "../..")).Path
|
||||
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) "dbx-win7-webview2-loader-$([Guid]::NewGuid())"
|
||||
$packagePath = Join-Path $temporaryRoot "Microsoft.Web.WebView2.$sdkVersion.nupkg"
|
||||
$extractedPath = Join-Path $temporaryRoot "extracted"
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $extractedPath -Force | Out-Null
|
||||
|
||||
$packageUrl = "https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/$sdkVersion"
|
||||
Write-Host "Downloading WebView2 SDK $sdkVersion for the Windows 7 loader..."
|
||||
Invoke-WebRequest -Uri $packageUrl -OutFile $packagePath -UseBasicParsing
|
||||
|
||||
$actualPackageSha256 = (Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actualPackageSha256 -ne $sdkPackageSha256) {
|
||||
throw "Unexpected WebView2 SDK package SHA256: $actualPackageSha256"
|
||||
}
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($packagePath, $extractedPath)
|
||||
|
||||
$legacyLoader = Join-Path $extractedPath "build/native/x64/WebView2LoaderStatic.lib"
|
||||
if (!(Test-Path -LiteralPath $legacyLoader -PathType Leaf)) {
|
||||
throw "WebView2 SDK $sdkVersion does not contain the x64 static loader."
|
||||
}
|
||||
|
||||
$legacyLoaderDll = Join-Path $extractedPath "build/native/x64/WebView2Loader.dll"
|
||||
if (!(Test-Path -LiteralPath $legacyLoaderDll -PathType Leaf)) {
|
||||
throw "WebView2 SDK $sdkVersion does not contain the x64 loader DLL."
|
||||
}
|
||||
|
||||
$actualLoaderSha256 = (Get-FileHash -LiteralPath $legacyLoader -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actualLoaderSha256 -ne $loaderSha256) {
|
||||
throw "Unexpected Windows 7 WebView2 loader SHA256: $actualLoaderSha256"
|
||||
}
|
||||
|
||||
Push-Location $repositoryRoot
|
||||
try {
|
||||
& cargo fetch --locked --target x86_64-win7-windows-msvc
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "cargo fetch failed while preparing the Windows 7 WebView2 loader."
|
||||
}
|
||||
|
||||
$metadataJson = & cargo metadata --locked --format-version 1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "cargo metadata failed while locating webview2-com-sys."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
$metadata = $metadataJson | ConvertFrom-Json
|
||||
$webView2Packages = @($metadata.packages | Where-Object {
|
||||
$_.name -eq "webview2-com-sys" -and $_.version -eq $webView2ComSysVersion
|
||||
})
|
||||
if ($webView2Packages.Count -ne 1) {
|
||||
throw "Expected exactly one webview2-com-sys $webView2ComSysVersion package, found $($webView2Packages.Count)."
|
||||
}
|
||||
|
||||
$crateRoot = Split-Path -Parent $webView2Packages[0].manifest_path
|
||||
$loaderDestination = Join-Path $crateRoot "x64/WebView2LoaderStatic.lib"
|
||||
if (!(Test-Path -LiteralPath $loaderDestination -PathType Leaf)) {
|
||||
throw "webview2-com-sys static loader does not exist: $loaderDestination"
|
||||
}
|
||||
|
||||
$existingLoaderSha256 = (Get-FileHash -LiteralPath $loaderDestination -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$knownLoaderHashes = @($upstreamLoaderSha256, $loaderSha256)
|
||||
if ($existingLoaderSha256 -notin $knownLoaderHashes) {
|
||||
throw "Refusing to replace an unknown webview2-com-sys loader SHA256: $existingLoaderSha256"
|
||||
}
|
||||
|
||||
Set-ItemProperty -LiteralPath $loaderDestination -Name IsReadOnly -Value $false
|
||||
Copy-Item -LiteralPath $legacyLoader -Destination $loaderDestination -Force
|
||||
|
||||
$installedLoaderSha256 = (Get-FileHash -LiteralPath $loaderDestination -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($installedLoaderSha256 -ne $loaderSha256) {
|
||||
throw "Windows 7 WebView2 loader replacement failed: $installedLoaderSha256"
|
||||
}
|
||||
|
||||
$probeDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "dbx-win7-webview2-loader-probe"
|
||||
New-Item -ItemType Directory -Path $probeDirectory -Force | Out-Null
|
||||
$probeLoader = Join-Path $probeDirectory "WebView2Loader.dll"
|
||||
Copy-Item -LiteralPath $legacyLoaderDll -Destination $probeLoader -Force
|
||||
|
||||
Write-Host "Prepared WebView2 SDK $sdkVersion static loader for Windows 7: $loaderDestination"
|
||||
Write-Host "Prepared WebView2 SDK $sdkVersion loader probe DLL: $probeLoader"
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $temporaryRoot) {
|
||||
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +1,76 @@
|
|||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$CacheRoot = (Join-Path $env:LOCALAPPDATA "tauri"),
|
||||
[string]$RuntimeDirectory = (Join-Path $PSScriptRoot "..\..\src-tauri\webview2-fixed-runtime"),
|
||||
[string]$DownloadDirectory = $env:RUNNER_TEMP
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$runtimeVersion = "109.0.1518.140"
|
||||
$runtimeUrl = "https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/updt/2023/09/microsoftedgestandaloneinstallerx64_1c890b4b8dd6b7c93da98ebdc08ecdc5e30e50cb.exe"
|
||||
$runtimeSha256 = "eac95c8095ec5f9971eade9827d8fb67fd251f5c16e702b5312d31067e39119b"
|
||||
$evergreenUrl = "https://go.microsoft.com/fwlink/?linkid=2124701"
|
||||
$runtimeVersion = "109.0.1518.78"
|
||||
$runtimeFolderName = "Microsoft.WebView2.FixedVersionRuntime.$runtimeVersion.x64"
|
||||
$archiveName = "$runtimeFolderName.cab"
|
||||
$runtimeUrl = "https://github.com/westinyang/WebView2RuntimeArchive/releases/download/$runtimeVersion/$archiveName"
|
||||
$runtimeSha256 = "7622281cf83de1a35e3a471f432f7a897d65f0a7d3975df08512b7b253dd45c7"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($CacheRoot)) {
|
||||
throw "A Tauri cache root is required."
|
||||
if ([string]::IsNullOrWhiteSpace($RuntimeDirectory)) {
|
||||
throw "A WebView2 fixed runtime directory is required."
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($DownloadDirectory)) {
|
||||
$DownloadDirectory = [System.IO.Path]::GetTempPath()
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $DownloadDirectory | Out-Null
|
||||
$downloadPath = Join-Path $DownloadDirectory "MicrosoftEdgeWebView2Runtime-$runtimeVersion-x64.exe"
|
||||
$archivePath = Join-Path $DownloadDirectory $archiveName
|
||||
|
||||
if (Test-Path $downloadPath) {
|
||||
$downloadHash = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if (Test-Path $archivePath) {
|
||||
$downloadHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($downloadHash -ne $runtimeSha256) {
|
||||
Remove-Item -LiteralPath $downloadPath -Force
|
||||
Remove-Item -LiteralPath $archivePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
if (!(Test-Path $downloadPath)) {
|
||||
Write-Host "Downloading WebView2 Runtime $runtimeVersion for Windows 7..."
|
||||
Invoke-WebRequest -Uri $runtimeUrl -OutFile $downloadPath
|
||||
if (!(Test-Path $archivePath)) {
|
||||
Write-Host "Downloading WebView2 fixed runtime $runtimeVersion for Windows 7..."
|
||||
Invoke-WebRequest -Uri $runtimeUrl -OutFile $archivePath
|
||||
}
|
||||
|
||||
$actualHash = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actualHash -ne $runtimeSha256) {
|
||||
throw "WebView2 Runtime SHA-256 mismatch. Expected $runtimeSha256, got $actualHash."
|
||||
throw "WebView2 fixed runtime SHA-256 mismatch. Expected $runtimeSha256, got $actualHash."
|
||||
}
|
||||
|
||||
# Tauri 2.11 does not expose an offline-installer path override. It resolves the
|
||||
# Evergreen URL and reuses a matching cache entry, so place the verified 109
|
||||
# installer at that exact location before bundling.
|
||||
$response = Invoke-WebRequest -Uri $evergreenUrl -Method Head
|
||||
$resolvedUrl = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
|
||||
$match = [regex]::Match(
|
||||
$resolvedUrl,
|
||||
"/filestreamingservice/files/(?<guid>[^/]+)/(?<filename>[^/?]+)"
|
||||
)
|
||||
if (!$match.Success) {
|
||||
throw "Unexpected Evergreen WebView2 URL: $resolvedUrl"
|
||||
# Microsoft no longer publishes old Fixed Version downloads. The archive is
|
||||
# accepted only when both its pinned hash and original Microsoft signature match.
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $archivePath
|
||||
if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or
|
||||
$null -eq $signature.SignerCertificate -or
|
||||
$signature.SignerCertificate.Subject -notmatch "Microsoft Corporation") {
|
||||
throw "WebView2 fixed runtime does not have a valid Microsoft signature."
|
||||
}
|
||||
|
||||
$cacheDirectory = Join-Path $CacheRoot (Join-Path "x64" $match.Groups["guid"].Value)
|
||||
$cachePath = Join-Path $cacheDirectory $match.Groups["filename"].Value
|
||||
New-Item -ItemType Directory -Force -Path $cacheDirectory | Out-Null
|
||||
Copy-Item -LiteralPath $downloadPath -Destination $cachePath -Force
|
||||
$extractDirectory = Join-Path $DownloadDirectory "dbx-webview2-fixed-runtime-$runtimeVersion"
|
||||
if (Test-Path $extractDirectory) {
|
||||
Remove-Item -LiteralPath $extractDirectory -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $extractDirectory | Out-Null
|
||||
|
||||
$cacheHash = (Get-FileHash -LiteralPath $cachePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($cacheHash -ne $runtimeSha256) {
|
||||
throw "Cached WebView2 Runtime SHA-256 mismatch. Expected $runtimeSha256, got $cacheHash."
|
||||
$expand = Join-Path $env:SystemRoot "System32\expand.exe"
|
||||
& $expand $archivePath "-F:*" $extractDirectory
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to extract WebView2 fixed runtime archive (exit code $LASTEXITCODE)."
|
||||
}
|
||||
|
||||
Write-Host "Prepared WebView2 Runtime $runtimeVersion at $cachePath"
|
||||
$extractedRuntime = Join-Path $extractDirectory $runtimeFolderName
|
||||
$runtimeExecutable = Join-Path $extractedRuntime "msedgewebview2.exe"
|
||||
if (!(Test-Path $runtimeExecutable)) {
|
||||
throw "Extracted WebView2 runtime is missing msedgewebview2.exe."
|
||||
}
|
||||
|
||||
if (Test-Path $RuntimeDirectory) {
|
||||
Remove-Item -LiteralPath $RuntimeDirectory -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $RuntimeDirectory) | Out-Null
|
||||
Move-Item -LiteralPath $extractedRuntime -Destination $RuntimeDirectory
|
||||
|
||||
Write-Host "Prepared WebView2 fixed runtime $runtimeVersion at $RuntimeDirectory"
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ jobs:
|
|||
timeout-minutes: 90
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTFLAGS: -C debuginfo=line-tables-only -C target-feature=+crt-static
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
|
|
@ -118,36 +119,73 @@ jobs:
|
|||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Rust for standard Windows
|
||||
uses: dtolnay/rust-toolchain@1.97.1
|
||||
|
||||
- name: Check standard Windows dependency path
|
||||
run: cargo check --locked --package dbx --no-default-features --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Setup Rust for Windows 7
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: nightly-2026-07-22
|
||||
components: rust-src
|
||||
|
||||
- name: Prepare Win7-compatible WebView2 loader
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/prepare-webview2-win7-loader.ps1
|
||||
|
||||
- name: Prepare WebView2 109 fixed runtime
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/prepare-webview2-win7-runtime.ps1
|
||||
|
||||
- name: Probe WebView2 109 fixed runtime
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/assert-webview2-win7-runtime.ps1
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm build
|
||||
|
||||
- name: Build DBX for Windows 7
|
||||
run: cargo build --locked --package dbx --release --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort
|
||||
|
||||
- name: Prepare WebView2 109 offline runtime
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/prepare-webview2-win7-runtime.ps1
|
||||
run: |
|
||||
$env:TAURI_CONFIG = Get-Content src-tauri/tauri.webview2-win7-fixed.conf.json -Raw
|
||||
cargo build --locked --package dbx --release --features custom-protocol --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort
|
||||
|
||||
- name: Bundle Windows 7 offline installer
|
||||
- name: Audit Windows 7 PE imports
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/assert-win7-pe-compat.ps1 -BinaryPath target/x86_64-win7-windows-msvc/release/dbx.exe
|
||||
|
||||
- name: Bundle Windows 7 fixed-runtime installer
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bundleDir = "target/x86_64-win7-windows-msvc/release/bundle/nsis"
|
||||
pnpm tauri bundle --bundles nsis --target x86_64-win7-windows-msvc --config src-tauri/tauri.webview2-win7-offline.conf.json
|
||||
pnpm tauri bundle --bundles nsis --target x86_64-win7-windows-msvc --config src-tauri/tauri.webview2-win7-fixed.conf.json
|
||||
$installer = Get-ChildItem $bundleDir -Filter "*.exe" |
|
||||
Sort-Object LastWriteTimeUtc -Descending |
|
||||
Select-Object -First 1
|
||||
if (!$installer) {
|
||||
Write-Error "Missing Windows 7 WebView2 offline installer in ${bundleDir}"
|
||||
Write-Error "Missing Windows 7 fixed-runtime installer in ${bundleDir}"
|
||||
exit 1
|
||||
}
|
||||
Get-FileHash -LiteralPath $installer.FullName -Algorithm SHA256
|
||||
|
||||
- name: Audit Windows 7 installer contents
|
||||
shell: pwsh
|
||||
run: |
|
||||
$installer = Get-ChildItem "target/x86_64-win7-windows-msvc/release/bundle/nsis" -Filter "*.exe" |
|
||||
Sort-Object LastWriteTimeUtc -Descending |
|
||||
Select-Object -First 1
|
||||
./.github/scripts/assert-win7-installer-content.ps1 -InstallerPath $installer.FullName
|
||||
|
||||
- name: Upload Windows 7 test installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: DBX-win7-fixed-runtime-test
|
||||
path: target/x86_64-win7-windows-msvc/release/bundle/nsis/*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
duckdb-windows-driver:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.duckdb_windows == 'true'
|
||||
|
|
@ -453,6 +491,7 @@ jobs:
|
|||
rust:
|
||||
- 'crates/**'
|
||||
- 'src-tauri/**'
|
||||
- 'vendor/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'rust-toolchain*'
|
||||
|
|
@ -488,16 +527,27 @@ jobs:
|
|||
- '.github/workflows/ci.yml'
|
||||
- '.github/workflows/update-nix-pnpm-hash.yml'
|
||||
windows_win7_bundle:
|
||||
- '.github/scripts/assert-win7-pe-compat.ps1'
|
||||
- '.github/scripts/assert-win7-installer-content.ps1'
|
||||
- '.github/scripts/assert-webview2-win7-runtime.ps1'
|
||||
- '.github/scripts/prepare-webview2-win7-loader.ps1'
|
||||
- '.github/scripts/prepare-webview2-win7-runtime.ps1'
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.github/workflows/release.yml'
|
||||
- 'src-tauri/tauri.webview2-win7-offline.conf.json'
|
||||
- 'src-tauri/tauri.webview2-win7-fixed.conf.json'
|
||||
- 'src-tauri/build.rs'
|
||||
- 'src-tauri/Cargo.toml'
|
||||
- 'src-tauri/windows/nsis/**'
|
||||
- 'src-tauri/src/commands/update.rs'
|
||||
- 'crates/dbx-core/Cargo.toml'
|
||||
- 'crates/dbx-core/src/db/postgres.rs'
|
||||
- 'crates/dbx-core/src/update.rs'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'vendor/ctor/**'
|
||||
- 'vendor/dirs-sys/**'
|
||||
- 'vendor/pageant/**'
|
||||
- 'vendor/wry/**'
|
||||
github_scripts:
|
||||
- '.github/scripts/**'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
|
@ -522,13 +572,13 @@ jobs:
|
|||
|
||||
while IFS= read -r file; do
|
||||
case "$file" in
|
||||
Cargo.toml|Cargo.lock|rust-toolchain*|.github/workflows/ci.yml|*/Cargo.toml)
|
||||
Cargo.toml|Cargo.lock|rust-toolchain*|.github/workflows/ci.yml|*/Cargo.toml|vendor/*)
|
||||
full=true
|
||||
break
|
||||
;;
|
||||
esac
|
||||
|
||||
done < <(git diff --name-only "$BASE_SHA" HEAD -- Cargo.toml Cargo.lock 'rust-toolchain*' crates src-tauri .github/workflows/ci.yml)
|
||||
done < <(git diff --name-only "$BASE_SHA" HEAD -- Cargo.toml Cargo.lock 'rust-toolchain*' crates src-tauri vendor .github/workflows/ci.yml)
|
||||
fi
|
||||
|
||||
echo "full=$full" >> "$GITHUB_OUTPUT"
|
||||
|
|
|
|||
|
|
@ -357,6 +357,7 @@ jobs:
|
|||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: ${{ secrets.SCCACHE_S3_BUCKET == '' && 'true' || 'false' }}
|
||||
RUSTFLAGS: -C debuginfo=line-tables-only -C target-feature=+crt-static
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
|
|
@ -377,6 +378,18 @@ jobs:
|
|||
toolchain: nightly-2026-07-22
|
||||
components: rust-src
|
||||
|
||||
- name: Prepare Win7-compatible WebView2 loader
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/prepare-webview2-win7-loader.ps1
|
||||
|
||||
- name: Prepare WebView2 109 fixed runtime
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/prepare-webview2-win7-runtime.ps1
|
||||
|
||||
- name: Probe WebView2 109 fixed runtime
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/assert-webview2-win7-runtime.ps1
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
with:
|
||||
|
|
@ -416,13 +429,16 @@ jobs:
|
|||
run: pnpm build
|
||||
|
||||
- name: Build DBX for Windows 7
|
||||
run: cargo build --locked --package dbx --release --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort
|
||||
|
||||
- name: Prepare WebView2 109 offline runtime
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/prepare-webview2-win7-runtime.ps1
|
||||
run: |
|
||||
$env:TAURI_CONFIG = Get-Content src-tauri/tauri.webview2-win7-fixed.conf.json -Raw
|
||||
cargo build --locked --package dbx --release --features custom-protocol --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort
|
||||
|
||||
- name: Bundle and upload Windows 7 offline installer
|
||||
- name: Audit Windows 7 PE imports
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/assert-win7-pe-compat.ps1 -BinaryPath target/x86_64-win7-windows-msvc/release/dbx.exe
|
||||
|
||||
- name: Bundle and upload Windows 7 fixed-runtime installer
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
@ -438,16 +454,18 @@ jobs:
|
|||
exit 1
|
||||
}
|
||||
|
||||
pnpm tauri bundle --bundles nsis --target x86_64-win7-windows-msvc --config src-tauri/tauri.webview2-win7-offline.conf.json
|
||||
pnpm tauri bundle --bundles nsis --target x86_64-win7-windows-msvc --config src-tauri/tauri.webview2-win7-fixed.conf.json
|
||||
|
||||
$installer = Get-ChildItem $bundleDir -Filter "*.exe" |
|
||||
Sort-Object LastWriteTimeUtc -Descending |
|
||||
Select-Object -First 1
|
||||
if (!$installer) {
|
||||
Write-Error "Missing Windows 7 WebView2 offline installer in ${bundleDir}"
|
||||
Write-Error "Missing Windows 7 fixed-runtime installer in ${bundleDir}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
./.github/scripts/assert-win7-installer-content.ps1 -InstallerPath $installer.FullName
|
||||
|
||||
Copy-Item $installer.FullName $offlineName -Force
|
||||
gh release upload "${env:GITHUB_REF_NAME}" $offlineName --repo "${env:GITHUB_REPOSITORY}" --clobber
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ CLAUDE.md
|
|||
dist/
|
||||
coverage/
|
||||
/target/
|
||||
src-tauri/webview2-fixed-runtime/
|
||||
plugins/jdbc/target/
|
||||
plugins/jdbc/dependency-reduced-pom.xml
|
||||
plugins/jdbc/lib/*.jar
|
||||
|
|
|
|||
|
|
@ -2028,8 +2028,6 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
|
|
@ -5264,8 +5262,6 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "pageant"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"byteorder",
|
||||
|
|
@ -5277,8 +5273,8 @@ dependencies = [
|
|||
"sha2 0.11.0",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"windows 0.61.3",
|
||||
"windows 0.62.2",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -10370,8 +10366,6 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
|||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.55.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2",
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -1,7 +1,7 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["src-tauri", "crates/dbx-core", "crates/dbx-web", "crates/dbx-mcp", "crates/dbx-cli"]
|
||||
exclude = ["vendor/ctor", "vendor/rumqttc"]
|
||||
exclude = ["vendor/ctor", "vendor/dirs-sys", "vendor/pageant", "vendor/rumqttc", "vendor/wry"]
|
||||
|
||||
[patch.crates-io]
|
||||
# Tauri 2.11 uses ctor 0.8, which excludes Rust's win7 vendor. This vendors
|
||||
|
|
@ -9,6 +9,14 @@ exclude = ["vendor/ctor", "vendor/rumqttc"]
|
|||
ctor = { path = "vendor/ctor" }
|
||||
# rumqttc 0.24 hardcodes MQTT 3.1.1; this patch adds MQTT 3.1 CONNECT encoding.
|
||||
rumqttc = { path = "vendor/rumqttc" }
|
||||
# Keep two small upstream crates on Win7-compatible API calls. Their current
|
||||
# Windows bindings otherwise import COMBASE or WinRT APIs unavailable on Win7.
|
||||
dirs-sys = { path = "vendor/dirs-sys" }
|
||||
pageant = { path = "vendor/pageant" }
|
||||
# Wry 0.55 probes and creates WebView2 with a null browser folder. Pass the
|
||||
# bundled Fixed Runtime path explicitly so Windows 7 does not fall back to an
|
||||
# unavailable system Runtime.
|
||||
wry = { path = "vendor/wry" }
|
||||
tokio-postgres = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "115f9fef10f0fc3669b5337955e4eb461fc349a6" }
|
||||
postgres-types = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "115f9fef10f0fc3669b5337955e4eb461fc349a6" }
|
||||
postgres-protocol = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "115f9fef10f0fc3669b5337955e4eb461fc349a6" }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,17 @@ import { describe, expect, it } from "vitest";
|
|||
import { shouldAbortWindowsWebView2RuntimeFallback } from "@/lib/app/windowsWebView2RuntimePolicy";
|
||||
|
||||
const template = readFileSync(resolve(process.cwd(), "src-tauri/windows/nsis/installer.nsi"), "utf8");
|
||||
const win7Config = JSON.parse(readFileSync(resolve(process.cwd(), "src-tauri/tauri.webview2-win7-fixed.conf.json"), "utf8"));
|
||||
const win7RuntimeScript = readFileSync(resolve(process.cwd(), ".github/scripts/prepare-webview2-win7-runtime.ps1"), "utf8");
|
||||
const win7PeAuditScript = readFileSync(resolve(process.cwd(), ".github/scripts/assert-win7-pe-compat.ps1"), "utf8");
|
||||
const win7RuntimeProbeScript = readFileSync(resolve(process.cwd(), ".github/scripts/assert-webview2-win7-runtime.ps1"), "utf8");
|
||||
const win7InstallerAuditScript = readFileSync(resolve(process.cwd(), ".github/scripts/assert-win7-installer-content.ps1"), "utf8");
|
||||
const appCargoToml = readFileSync(resolve(process.cwd(), "src-tauri/Cargo.toml"), "utf8");
|
||||
const workspaceCargoToml = readFileSync(resolve(process.cwd(), "Cargo.toml"), "utf8");
|
||||
const appBuildScript = readFileSync(resolve(process.cwd(), "src-tauri/build.rs"), "utf8");
|
||||
const wryWebView2Source = readFileSync(resolve(process.cwd(), "vendor/wry/src/webview2/mod.rs"), "utf8");
|
||||
const ciWorkflow = readFileSync(resolve(process.cwd(), ".github/workflows/ci.yml"), "utf8");
|
||||
const releaseWorkflow = readFileSync(resolve(process.cwd(), ".github/workflows/release.yml"), "utf8");
|
||||
|
||||
describe("Windows offline installer template", () => {
|
||||
it.each([
|
||||
|
|
@ -63,3 +74,72 @@ describe("Windows offline installer template", () => {
|
|||
expect(template).toContain('ReadRegStr ${RESULT} HKCU "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\${WEBVIEW2APPGUID}" "pv"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Windows 7 fixed WebView2 runtime bundle", () => {
|
||||
it("bundles a fixed runtime instead of invoking a system Runtime installer", () => {
|
||||
expect(win7Config.bundle.windows.webviewInstallMode).toEqual({
|
||||
type: "fixedRuntime",
|
||||
path: "webview2-fixed-runtime",
|
||||
});
|
||||
expect(win7RuntimeScript).toContain("Microsoft.WebView2.FixedVersionRuntime.$runtimeVersion.x64");
|
||||
expect(win7RuntimeScript).toContain("msedgewebview2.exe");
|
||||
expect(win7RuntimeScript).not.toContain("microsoftedgestandaloneinstaller");
|
||||
expect(win7RuntimeScript).not.toContain("go.microsoft.com/fwlink");
|
||||
});
|
||||
|
||||
it("pins the archived Microsoft runtime before extracting it", () => {
|
||||
expect(win7RuntimeScript).toContain('$runtimeVersion = "109.0.1518.78"');
|
||||
expect(win7RuntimeScript).toContain('$runtimeSha256 = "7622281cf83de1a35e3a471f432f7a897d65f0a7d3975df08512b7b253dd45c7"');
|
||||
expect(win7RuntimeScript).toContain("Get-FileHash -LiteralPath $archivePath -Algorithm SHA256");
|
||||
expect(win7RuntimeScript).toContain("Get-AuthenticodeSignature -LiteralPath $archivePath");
|
||||
expect(win7RuntimeScript).toContain('$signature.SignerCertificate.Subject -notmatch "Microsoft Corporation"');
|
||||
expect(win7RuntimeScript).toContain('& $expand $archivePath "-F:*" $extractDirectory');
|
||||
});
|
||||
|
||||
it("rejects executable imports that cannot load on Windows 7", () => {
|
||||
expect(win7PeAuditScript).toContain('"combase.dll"');
|
||||
expect(win7PeAuditScript).toContain('"api-ms-win-core-winrt-"');
|
||||
expect(win7PeAuditScript).toContain('"CoIncrementMTAUsage"');
|
||||
expect(win7PeAuditScript).toContain('"GetSystemTimePreciseAsFileTime"');
|
||||
expect(win7PeAuditScript).toContain('"VCRUNTIME140.dll"');
|
||||
expect(win7PeAuditScript).toContain('"MSVCP140.dll"');
|
||||
expect(win7PeAuditScript).toContain('"ucrtbase.dll"');
|
||||
expect(win7PeAuditScript).toContain('"api-ms-win-crt-"');
|
||||
expect(win7PeAuditScript).toContain("dumpbin.exe");
|
||||
});
|
||||
|
||||
it("probes the fixed runtime through the Win7-compatible loader", () => {
|
||||
expect(win7RuntimeProbeScript).toContain("GetAvailableCoreWebView2BrowserVersionString");
|
||||
expect(win7RuntimeProbeScript).toContain('$ExpectedVersion = "109.0.1518.78"');
|
||||
expect(win7RuntimeProbeScript).toContain("msedgewebview2.exe");
|
||||
expect(ciWorkflow).toContain("./.github/scripts/assert-webview2-win7-runtime.ps1");
|
||||
expect(releaseWorkflow).toContain("./.github/scripts/assert-webview2-win7-runtime.ps1");
|
||||
});
|
||||
|
||||
it("passes the configured fixed-runtime folder to WebView2 discovery and creation", () => {
|
||||
expect(workspaceCargoToml).toContain('wry = { path = "vendor/wry" }');
|
||||
expect(wryWebView2Source).toContain('std::env::var_os("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER")');
|
||||
expect(wryWebView2Source).toContain(`CreateCoreWebView2EnvironmentWithOptions(
|
||||
browser_executable_folder_ptr,`);
|
||||
expect(wryWebView2Source).toContain("GetAvailableCoreWebView2BrowserVersionString(browser_executable_folder_ptr, &mut versioninfo)");
|
||||
});
|
||||
|
||||
it("audits the files produced by the silent Win7 installer", () => {
|
||||
expect(win7InstallerAuditScript).toContain('"webview2-fixed-runtime\\msedgewebview2.exe"');
|
||||
expect(win7InstallerAuditScript).toContain('"dbx.exe"');
|
||||
expect(ciWorkflow).toContain("./.github/scripts/assert-win7-installer-content.ps1");
|
||||
expect(releaseWorkflow).toContain("./.github/scripts/assert-win7-installer-content.ps1");
|
||||
});
|
||||
|
||||
it("builds the Windows 7 executable with the production custom protocol", () => {
|
||||
expect(appCargoToml).toContain('custom-protocol = ["tauri/custom-protocol"]');
|
||||
expect(appBuildScript).toContain("CARGO_FEATURE_CUSTOM_PROTOCOL");
|
||||
expect(appBuildScript).toContain("CARGO_CFG_TARGET_VENDOR");
|
||||
expect(ciWorkflow).toContain("--release --features custom-protocol --target x86_64-win7-windows-msvc");
|
||||
expect(releaseWorkflow).toContain("--release --features custom-protocol --target x86_64-win7-windows-msvc");
|
||||
expect(ciWorkflow).toContain("TAURI_CONFIG = Get-Content src-tauri/tauri.webview2-win7-fixed.conf.json -Raw");
|
||||
expect(releaseWorkflow).toContain("TAURI_CONFIG = Get-Content src-tauri/tauri.webview2-win7-fixed.conf.json -Raw");
|
||||
expect(ciWorkflow).toContain("target-feature=+crt-static");
|
||||
expect(releaseWorkflow).toContain("target-feature=+crt-static");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ tiberius = { version = "0.12.3", default-features = false, features = ["tds73",
|
|||
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "socks"] }
|
||||
prometheus-parse = "=0.2.5"
|
||||
futures = "0.3"
|
||||
iana-time-zone = "0.1"
|
||||
mongodb = "3.2.5"
|
||||
rumqttc = { version = "0.24", features = ["websocket"], optional = true }
|
||||
russh = "0.60"
|
||||
|
|
@ -80,3 +79,6 @@ notify = { version = "7", default-features = false, features = ["macos_kqueue"]
|
|||
arc-swap = "1"
|
||||
insta = { version = "1", features = ["json", "glob"] }
|
||||
tempfile = "3"
|
||||
|
||||
[target.'cfg(not(all(windows, target_vendor = "win7")))'.dependencies]
|
||||
iana-time-zone = "0.1"
|
||||
|
|
|
|||
|
|
@ -1441,11 +1441,27 @@ async fn stream_query_rows_text_on_client(
|
|||
}
|
||||
|
||||
pub async fn connect(url: &str, fallback_timeout: Duration) -> Result<Pool, String> {
|
||||
let timezone = iana_time_zone::get_timezone().unwrap_or_else(|_| "UTC".to_string());
|
||||
connect_with_local_timezone(url, fallback_timeout, &timezone).await
|
||||
#[cfg(all(windows, target_vendor = "win7"))]
|
||||
{
|
||||
connect_with_optional_local_timezone(url, fallback_timeout, None).await
|
||||
}
|
||||
|
||||
#[cfg(not(all(windows, target_vendor = "win7")))]
|
||||
{
|
||||
let timezone = iana_time_zone::get_timezone().unwrap_or_else(|_| "UTC".to_string());
|
||||
connect_with_local_timezone(url, fallback_timeout, &timezone).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_with_local_timezone(url: &str, fallback_timeout: Duration, timezone: &str) -> Result<Pool, String> {
|
||||
connect_with_optional_local_timezone(url, fallback_timeout, Some(timezone)).await
|
||||
}
|
||||
|
||||
async fn connect_with_optional_local_timezone(
|
||||
url: &str,
|
||||
fallback_timeout: Duration,
|
||||
timezone: Option<&str>,
|
||||
) -> Result<Pool, String> {
|
||||
let url_with_keepalive = inject_postgres_keepalive_params(url);
|
||||
let postgres_url = postgres_connection_url(&url_with_keepalive)?;
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
|
@ -1488,7 +1504,9 @@ async fn connect_with_local_timezone(url: &str, fallback_timeout: Duration, time
|
|||
let client =
|
||||
pool.get().await.map_err(|e| format!("PostgreSQL connection failed: {}", pg_pool_error_to_string(e)))?;
|
||||
if !pg_url_has_timezone_setting(url) {
|
||||
set_automatic_postgres_timezone(&client, timezone).await?;
|
||||
if let Some(timezone) = timezone {
|
||||
set_automatic_postgres_timezone(&client, timezone).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||
|
||||
[features]
|
||||
default = ["duckdb-sidecar", "mq-admin", "sqlite-sqlcipher", "system-fonts"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
duckdb-sidecar = ["dbx-core/duckdb-sidecar"]
|
||||
mq-admin = ["dbx-core/mq-admin"]
|
||||
sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
fn main() {
|
||||
let is_win7_target = std::env::var("CARGO_CFG_TARGET_VENDOR").as_deref() == Ok("win7");
|
||||
if is_win7_target && std::env::var_os("CARGO_FEATURE_CUSTOM_PROTOCOL").is_none() {
|
||||
panic!("Windows 7 release builds must enable the custom-protocol feature");
|
||||
}
|
||||
|
||||
// Force rebuild to re-embed frontend assets
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
"createUpdaterArtifacts": false,
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"silent": true,
|
||||
"type": "offlineInstaller"
|
||||
"type": "fixedRuntime",
|
||||
"path": "webview2-fixed-runtime"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
authors = ["Simon Ochsenreither <simon@ochsenreither.de>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "System-level helper functions for the dirs and directories crates."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/dirs-dev/dirs-sys-rs"
|
||||
|
||||
[lib]
|
||||
name = "dirs_sys"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.option-ext]
|
||||
version = "0.2.0"
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies.redox_users]
|
||||
version = "0.5"
|
||||
default-features = false
|
||||
|
||||
[target."cfg(unix)".dependencies.libc]
|
||||
version = "0.2"
|
||||
|
||||
[target.'cfg(all(windows, target_vendor = "win7"))'.dependencies.windows-sys]
|
||||
version = "=0.61.2"
|
||||
features = [
|
||||
"Win32_UI_Shell",
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
]
|
||||
|
||||
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies.windows-sys]
|
||||
version = "=0.61.2"
|
||||
features = [
|
||||
"Win32_UI_Shell",
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_System_Com",
|
||||
]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# DBX Windows 7 compatibility patch
|
||||
|
||||
This is `dirs-sys` 0.5.0 with `CoTaskMemFree` linked directly from
|
||||
`ole32.dll` only for the Win7 target. `windows-sys` 0.61 links that function
|
||||
from `combase.dll`, which is unavailable on Windows 7. Other Windows targets
|
||||
retain the upstream `windows-sys` implementation and dependency features.
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2018-2019 dirs-rs contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
[](https://crates.io/crates/dirs-sys)
|
||||
[](https://docs.rs/dirs-sys/)
|
||||

|
||||
|
||||
# `dirs-sys`
|
||||
|
||||
System-level helper functions for the [`dirs`](https://github.com/dirs-dev/dirs-rs)
|
||||
and [`directories`](https://github.com/dirs-dev/directories-rs) crates.
|
||||
|
||||
_Do not use this library directly, use [`dirs`](https://github.com/dirs-dev/dirs-rs)
|
||||
or [`directories`](https://github.com/dirs-dev/directories-rs)._
|
||||
|
||||
## Compatibility
|
||||
|
||||
This crate only exists to facilitate code sharing between [`dirs`](https://github.com/dirs-dev/dirs-rs)
|
||||
and [`directories`](https://github.com/dirs-dev/directories-rs).
|
||||
|
||||
There are no compatibility guarantees whatsoever.
|
||||
Functions may change or disappear without warning or any kind of deprecation period.
|
||||
|
||||
## Platforms
|
||||
|
||||
This library is written in Rust, and supports Linux, Redox, macOS and Windows.
|
||||
Other platforms are also supported; they use the Linux conventions.
|
||||
|
||||
## Build
|
||||
|
||||
It's possible to cross-compile this library if the necessary toolchains are installed with rustup.
|
||||
This is helpful to ensure a change has not broken compilation on a different platform.
|
||||
|
||||
The following commands will build this library on Linux, macOS and Windows:
|
||||
|
||||
```
|
||||
cargo build --target=x86_64-unknown-linux-gnu
|
||||
cargo build --target=x86_64-pc-windows-gnu
|
||||
cargo build --target=x86_64-apple-darwin
|
||||
cargo build --target=x86_64-unknown-redox
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0
|
||||
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license
|
||||
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
extern crate option_ext;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// we don't need to explicitly handle empty strings in the code above,
|
||||
// because an empty string is not considered to be a absolute path here.
|
||||
pub fn is_absolute_path(path: OsString) -> Option<PathBuf> {
|
||||
let path = PathBuf::from(path);
|
||||
if path.is_absolute() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
extern crate libc;
|
||||
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
mod target_unix_not_redox {
|
||||
|
||||
use std::env;
|
||||
use std::ffi::{CStr, OsString};
|
||||
use std::mem;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
|
||||
use super::libc;
|
||||
|
||||
// https://github.com/rust-lang/rust/blob/2682b88c526d493edeb2d3f2df358f44db69b73f/library/std/src/sys/unix/os.rs#L595
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
return env::var_os("HOME")
|
||||
.and_then(|h| if h.is_empty() { None } else { Some(h) })
|
||||
.or_else(|| unsafe { fallback() })
|
||||
.map(PathBuf::from);
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios", target_os = "emscripten"))]
|
||||
unsafe fn fallback() -> Option<OsString> {
|
||||
None
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "emscripten")))]
|
||||
unsafe fn fallback() -> Option<OsString> {
|
||||
let amt = match libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) {
|
||||
n if n < 0 => 512 as usize,
|
||||
n => n as usize,
|
||||
};
|
||||
let mut buf = Vec::with_capacity(amt);
|
||||
let mut passwd: libc::passwd = mem::zeroed();
|
||||
let mut result = ptr::null_mut();
|
||||
match libc::getpwuid_r(
|
||||
libc::getuid(),
|
||||
&mut passwd,
|
||||
buf.as_mut_ptr(),
|
||||
buf.capacity(),
|
||||
&mut result,
|
||||
) {
|
||||
0 if !result.is_null() => {
|
||||
let ptr = passwd.pw_dir as *const _;
|
||||
let bytes = CStr::from_ptr(ptr).to_bytes();
|
||||
if bytes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(OsStringExt::from_vec(bytes.to_vec()))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
pub use self::target_unix_not_redox::home_dir;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
extern crate redox_users;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
mod target_redox {
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::redox_users::{All, AllUsers, Config};
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
let current_uid = redox_users::get_uid().ok()?;
|
||||
let users = AllUsers::basic(Config::default()).ok()?;
|
||||
let user = users.get_by_id(current_uid)?;
|
||||
|
||||
Some(PathBuf::from(user.home.clone()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
pub use self::target_redox::home_dir;
|
||||
|
||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
|
||||
mod xdg_user_dirs;
|
||||
|
||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
|
||||
mod target_unix_not_mac {
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{home_dir, is_absolute_path};
|
||||
use super::xdg_user_dirs;
|
||||
|
||||
fn user_dir_file(home_dir: &Path) -> PathBuf {
|
||||
env::var_os("XDG_CONFIG_HOME").and_then(is_absolute_path).unwrap_or_else(|| home_dir.join(".config")).join("user-dirs.dirs")
|
||||
}
|
||||
|
||||
// this could be optimized further to not create a map and instead retrieve the requested path only
|
||||
pub fn user_dir(user_dir_name: &str) -> Option<PathBuf> {
|
||||
if let Some(home_dir) = home_dir() {
|
||||
xdg_user_dirs::single(&home_dir, &user_dir_file(&home_dir), user_dir_name).remove(user_dir_name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_dirs(home_dir_path: &Path) -> HashMap<String, PathBuf> {
|
||||
xdg_user_dirs::all(home_dir_path, &user_dir_file(home_dir_path))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
|
||||
pub use self::target_unix_not_mac::{user_dir, user_dirs};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
extern crate windows_sys as windows;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod target_windows {
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use std::path::PathBuf;
|
||||
use std::slice;
|
||||
|
||||
use super::windows::Win32::UI::Shell;
|
||||
|
||||
#[cfg(target_vendor = "win7")]
|
||||
#[link(name = "ole32")]
|
||||
extern "system" {
|
||||
fn CoTaskMemFree(pv: *const c_void);
|
||||
}
|
||||
|
||||
pub fn known_folder(folder_id: windows::core::GUID) -> Option<PathBuf> {
|
||||
unsafe {
|
||||
let mut path_ptr: windows::core::PWSTR = std::ptr::null_mut();
|
||||
let result = Shell::SHGetKnownFolderPath(
|
||||
&folder_id,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
&mut path_ptr
|
||||
);
|
||||
if result == 0 {
|
||||
let len = windows::Win32::Globalization::lstrlenW(path_ptr) as usize;
|
||||
let path = slice::from_raw_parts(path_ptr, len);
|
||||
let ostr: OsString = OsStringExt::from_wide(path);
|
||||
#[cfg(target_vendor = "win7")]
|
||||
CoTaskMemFree(path_ptr as *const c_void);
|
||||
#[cfg(not(target_vendor = "win7"))]
|
||||
windows::Win32::System::Com::CoTaskMemFree(path_ptr as *const c_void);
|
||||
Some(PathBuf::from(ostr))
|
||||
} else {
|
||||
#[cfg(target_vendor = "win7")]
|
||||
CoTaskMemFree(path_ptr as *const c_void);
|
||||
#[cfg(not(target_vendor = "win7"))]
|
||||
windows::Win32::System::Com::CoTaskMemFree(path_ptr as *const c_void);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn known_folder_profile() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Profile)
|
||||
}
|
||||
|
||||
pub fn known_folder_roaming_app_data() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_RoamingAppData)
|
||||
}
|
||||
|
||||
pub fn known_folder_local_app_data() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_LocalAppData)
|
||||
}
|
||||
|
||||
pub fn known_folder_music() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Music)
|
||||
}
|
||||
|
||||
pub fn known_folder_desktop() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Desktop)
|
||||
}
|
||||
|
||||
pub fn known_folder_documents() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Documents)
|
||||
}
|
||||
|
||||
pub fn known_folder_downloads() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Downloads)
|
||||
}
|
||||
|
||||
pub fn known_folder_pictures() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Pictures)
|
||||
}
|
||||
|
||||
pub fn known_folder_public() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Public)
|
||||
}
|
||||
pub fn known_folder_templates() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Templates)
|
||||
}
|
||||
pub fn known_folder_videos() -> Option<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Videos)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use self::target_windows::{
|
||||
known_folder, known_folder_profile, known_folder_roaming_app_data, known_folder_local_app_data,
|
||||
known_folder_music, known_folder_desktop, known_folder_documents, known_folder_downloads,
|
||||
known_folder_pictures, known_folder_public, known_folder_templates, known_folder_videos
|
||||
};
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str;
|
||||
|
||||
use option_ext::OptionExt;
|
||||
|
||||
/// Returns all XDG user directories obtained from $(XDG_CONFIG_HOME)/user-dirs.dirs.
|
||||
pub fn all(home_dir_path: &Path, user_dir_file_path: &Path) -> HashMap<String, PathBuf> {
|
||||
let bytes = read_all(user_dir_file_path).unwrap_or(Vec::new());
|
||||
parse_user_dirs(home_dir_path, None, &bytes)
|
||||
}
|
||||
|
||||
/// Returns a single XDG user directory obtained from $(XDG_CONFIG_HOME)/user-dirs.dirs.
|
||||
pub fn single(home_dir_path: &Path, user_dir_file_path: &Path, user_dir_name: &str) -> HashMap<String, PathBuf> {
|
||||
let bytes = read_all(user_dir_file_path).unwrap_or(Vec::new());
|
||||
parse_user_dirs(home_dir_path, Some(user_dir_name), &bytes)
|
||||
}
|
||||
|
||||
fn parse_user_dirs(home_dir: &Path, user_dir: Option<&str>, bytes: &[u8]) -> HashMap<String, PathBuf> {
|
||||
let mut user_dirs = HashMap::new();
|
||||
|
||||
for line in bytes.split(|b| *b == b'\n') {
|
||||
let mut single_dir_found = false;
|
||||
let (key, value) = match split_once(line, b'=') {
|
||||
Some(kv) => kv,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let key = trim_blank(key);
|
||||
let key = if key.starts_with(b"XDG_") && key.ends_with(b"_DIR") {
|
||||
match str::from_utf8(&key[4..key.len()-4]) {
|
||||
Ok(key) =>
|
||||
if user_dir.contains(&key) {
|
||||
single_dir_found = true;
|
||||
key
|
||||
} else if user_dir.is_none() {
|
||||
key
|
||||
} else {
|
||||
continue
|
||||
},
|
||||
Err(_) => continue,
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
};
|
||||
|
||||
// xdg-user-dirs-update uses double quotes and we don't support anything else.
|
||||
let value = trim_blank(value);
|
||||
let mut value = if value.starts_with(b"\"") && value.ends_with(b"\"") {
|
||||
&value[1..value.len()-1]
|
||||
} else {
|
||||
continue
|
||||
};
|
||||
|
||||
// Path should be either relative to the home directory or absolute.
|
||||
let is_relative = if value == b"$HOME/" {
|
||||
// "Note: To disable a directory, point it to the homedir."
|
||||
// Source: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/
|
||||
// Additionally directory is reassigned to homedir when removed.
|
||||
continue
|
||||
} else if value.starts_with(b"$HOME/") {
|
||||
value = &value[b"$HOME/".len()..];
|
||||
true
|
||||
} else if value.starts_with(b"/") {
|
||||
false
|
||||
} else {
|
||||
continue
|
||||
};
|
||||
|
||||
let value = OsString::from_vec(shell_unescape(value));
|
||||
|
||||
let path = if is_relative {
|
||||
let mut path = PathBuf::from(&home_dir);
|
||||
path.push(value);
|
||||
path
|
||||
} else {
|
||||
PathBuf::from(value)
|
||||
};
|
||||
|
||||
user_dirs.insert(key.to_owned(), path);
|
||||
if single_dir_found {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
user_dirs
|
||||
}
|
||||
|
||||
/// Reads the entire contents of a file into a byte vector.
|
||||
fn read_all(path: &Path) -> io::Result<Vec<u8>> {
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut bytes = Vec::with_capacity(1024);
|
||||
file.read_to_end(&mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Returns bytes before and after first occurrence of separator.
|
||||
fn split_once(bytes: &[u8], separator: u8) -> Option<(&[u8], &[u8])> {
|
||||
bytes.iter().position(|b| *b == separator).map(|i| {
|
||||
(&bytes[..i], &bytes[i+1..])
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a slice with leading and trailing <blank> characters removed.
|
||||
fn trim_blank(bytes: &[u8]) -> &[u8] {
|
||||
// Trim leading <blank> characters.
|
||||
let i = bytes.iter().cloned().take_while(|b| *b == b' ' || *b == b'\t').count();
|
||||
let bytes = &bytes[i..];
|
||||
|
||||
// Trim trailing <blank> characters.
|
||||
let i = bytes.iter().cloned().rev().take_while(|b| *b == b' ' || *b == b'\t').count();
|
||||
&bytes[..bytes.len()-i]
|
||||
}
|
||||
|
||||
/// Unescape bytes escaped with POSIX shell double-quotes rules (as used by xdg-user-dirs-update).
|
||||
fn shell_unescape(escaped: &[u8]) -> Vec<u8> {
|
||||
// We assume that byte string was created by xdg-user-dirs-update which
|
||||
// escapes all characters that might potentially have special meaning,
|
||||
// so there is no need to check if backslash is actually followed by
|
||||
// $ ` " \ or a <newline>.
|
||||
|
||||
let mut unescaped: Vec<u8> = Vec::with_capacity(escaped.len());
|
||||
let mut i = escaped.iter().cloned();
|
||||
|
||||
while let Some(b) = i.next() {
|
||||
if b == b'\\' {
|
||||
if let Some(b) = i.next() {
|
||||
unescaped.push(b);
|
||||
}
|
||||
} else {
|
||||
unescaped.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
unescaped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{parse_user_dirs, shell_unescape, split_once, trim_blank};
|
||||
|
||||
#[test]
|
||||
fn test_trim_blank() {
|
||||
assert_eq!(b"x", trim_blank(b"x"));
|
||||
assert_eq!(b"", trim_blank(b" \t "));
|
||||
assert_eq!(b"hello there", trim_blank(b" \t hello there \t "));
|
||||
assert_eq!(b"\r\n", trim_blank(b"\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_once() {
|
||||
assert_eq!(None, split_once(b"a b c", b'='));
|
||||
assert_eq!(Some((b"before".as_ref(), b"after".as_ref())), split_once(b"before=after", b'='));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shell_unescape() {
|
||||
assert_eq!(b"abc", shell_unescape(b"abc").as_slice());
|
||||
assert_eq!(b"x\\y$z`", shell_unescape(b"x\\\\y\\$z\\`").as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty() {
|
||||
assert_eq!(HashMap::new(), parse_user_dirs(Path::new("/root/"), None, b""));
|
||||
assert_eq!(HashMap::new(), parse_user_dirs(Path::new("/root/"), Some("MUSIC"), b""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_absolute_path_is_accepted() {
|
||||
let mut dirs = HashMap::new();
|
||||
dirs.insert("MUSIC".to_owned(), PathBuf::from("/media/music"));
|
||||
let bytes = br#"XDG_MUSIC_DIR="/media/music""#;
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), None, bytes));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), Some("MUSIC"), bytes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_path_is_rejected() {
|
||||
let dirs = HashMap::new();
|
||||
let bytes = br#"XDG_MUSIC_DIR="music""#;
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), None, bytes));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), Some("MUSIC"), bytes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_to_home() {
|
||||
let mut dirs = HashMap::new();
|
||||
dirs.insert("MUSIC".to_owned(), PathBuf::from("/home/john/Music"));
|
||||
let bytes = br#"XDG_MUSIC_DIR="$HOME/Music""#;
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), None, bytes));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), Some("MUSIC"), bytes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_directory() {
|
||||
let dirs = HashMap::new();
|
||||
let bytes = br#"XDG_MUSIC_DIR="$HOME/""#;
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), None, bytes));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), Some("MUSIC"), bytes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_user_dirs() {
|
||||
let mut dirs: HashMap<String, PathBuf> = HashMap::new();
|
||||
dirs.insert("DESKTOP".to_string(), PathBuf::from("/home/bob/Desktop"));
|
||||
dirs.insert("DOWNLOAD".to_string(), PathBuf::from("/home/bob/Downloads"));
|
||||
dirs.insert("PICTURES".to_string(), PathBuf::from("/home/eve/pics"));
|
||||
|
||||
let bytes = br#"
|
||||
# This file is written by xdg-user-dirs-update
|
||||
# If you want to change or add directories, just edit the line you're
|
||||
# interested in. All local changes will be retained on the next run.
|
||||
# Format is XDG_xxx_DIR="$HOME/yyy", where yyy is a shell-escaped
|
||||
# homedir-relative path, or XDG_xxx_DIR="/yyy", where /yyy is an
|
||||
# absolute path. No other format is supported.
|
||||
XDG_DESKTOP_DIR="$HOME/Desktop"
|
||||
XDG_DOWNLOAD_DIR="$HOME/Downloads"
|
||||
XDG_TEMPLATES_DIR=""
|
||||
XDG_PUBLICSHARE_DIR="$HOME"
|
||||
XDG_DOCUMENTS_DIR="$HOME/"
|
||||
XDG_PICTURES_DIR="/home/eve/pics"
|
||||
XDG_VIDEOS_DIR="$HOxyzME/Videos"
|
||||
"#;
|
||||
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), None, bytes));
|
||||
|
||||
let mut dirs: HashMap<String, PathBuf> = HashMap::new();
|
||||
dirs.insert("DESKTOP".to_string(), PathBuf::from("/home/bob/Desktop"));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("DESKTOP"), bytes));
|
||||
|
||||
let mut dirs: HashMap<String, PathBuf> = HashMap::new();
|
||||
dirs.insert("PICTURES".to_string(), PathBuf::from("/home/eve/pics"));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("PICTURES"), bytes));
|
||||
|
||||
let dirs: HashMap<String, PathBuf> = HashMap::new();
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("TEMPLATES"), bytes));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("PUBLICSHARE"), bytes));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("DOCUMENTS"), bytes));
|
||||
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("VIDEOS"), bytes));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
name = "pageant"
|
||||
version = "0.2.1"
|
||||
authors = ["Eugene <inbox@null.page>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Pageant SSH agent transport client."
|
||||
documentation = "https://docs.rs/pageant"
|
||||
readme = false
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/warp-tech/russh"
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"wmmessage",
|
||||
"namedpipes",
|
||||
]
|
||||
namedpipes = [
|
||||
"tokio/net",
|
||||
"tokio/time",
|
||||
"dep:sha2",
|
||||
]
|
||||
wmmessage = [
|
||||
"tokio/rt",
|
||||
"tokio/io-util",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "pageant"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2.0.18"
|
||||
|
||||
[target."cfg(windows)".dependencies.base16ct]
|
||||
version = "1"
|
||||
features = ["alloc"]
|
||||
|
||||
[target."cfg(windows)".dependencies.byteorder]
|
||||
version = "1.4"
|
||||
|
||||
[target."cfg(windows)".dependencies.bytes]
|
||||
version = "1.7"
|
||||
|
||||
[target."cfg(windows)".dependencies.delegate]
|
||||
version = "0.13"
|
||||
|
||||
[target."cfg(windows)".dependencies.futures]
|
||||
version = "0.3"
|
||||
|
||||
[target."cfg(windows)".dependencies.log]
|
||||
version = "0.4.11"
|
||||
|
||||
[target."cfg(windows)".dependencies.rand]
|
||||
version = "0.10"
|
||||
features = ["thread_rng"]
|
||||
|
||||
[target."cfg(windows)".dependencies.sha2]
|
||||
version = "0.11"
|
||||
features = ["oid"]
|
||||
optional = true
|
||||
|
||||
[target."cfg(windows)".dependencies.tokio]
|
||||
version = "1.17.0"
|
||||
|
||||
[target.'cfg(all(windows, target_vendor = "win7"))'.dependencies.windows-win7]
|
||||
version = "=0.61.3"
|
||||
features = [
|
||||
"Win32_Security",
|
||||
"Win32_Security_Authentication_Identity",
|
||||
"Win32_Security_Cryptography",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_DataExchange",
|
||||
]
|
||||
package = "windows"
|
||||
|
||||
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies.windows-modern]
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Win32_Security",
|
||||
"Win32_Security_Authentication_Identity",
|
||||
"Win32_Security_Cryptography",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_DataExchange",
|
||||
]
|
||||
package = "windows"
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# DBX Windows 7 compatibility patch
|
||||
|
||||
This is `pageant` 0.2.1 with WinRT `HSTRING` removed from Pageant window and
|
||||
mapping names. It uses null-terminated UTF-16 strings instead. The Win7 target
|
||||
uses Windows bindings 0.61.3 to avoid COMBASE and WinRT imports unavailable on
|
||||
Windows 7, while other Windows targets retain the upstream 0.62 dependency.
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Pageant not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("Buffer overflow")]
|
||||
Overflow,
|
||||
|
||||
#[error("No response from Pageant")]
|
||||
NoResponse,
|
||||
|
||||
#[error("Invalid Cookie")]
|
||||
InvalidCookie,
|
||||
|
||||
#[error("NamedPipe keeps returning Busy")]
|
||||
PipeBusy,
|
||||
|
||||
#[error("Invalid Username")]
|
||||
InvalidUsername,
|
||||
|
||||
#[cfg(windows)]
|
||||
#[error(transparent)]
|
||||
WindowsError(#[from] crate::windows::core::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Multiple Errors")]
|
||||
Multiple(Vec<Self>),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn from_win32() -> Self {
|
||||
#[cfg(target_vendor = "win7")]
|
||||
let error = crate::windows::core::Error::from_win32();
|
||||
#[cfg(not(target_vendor = "win7"))]
|
||||
let error = crate::windows::core::Error::from_thread();
|
||||
|
||||
Self::WindowsError(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
use std::pin::Pin;
|
||||
|
||||
use log::debug;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
use crate::Error;
|
||||
#[cfg(all(windows, feature = "namedpipes"))]
|
||||
use crate::namedpipes;
|
||||
#[cfg(all(windows, feature = "wmmessage"))]
|
||||
use crate::wmmessage;
|
||||
|
||||
/// Pageant transport stream (using one of the available transport implementations).
|
||||
/// Implements [AsyncRead] and [AsyncWrite].
|
||||
pub enum PageantStream {
|
||||
#[cfg(all(windows, feature = "wmmessage"))]
|
||||
WmMessage(wmmessage::PageantStream),
|
||||
#[cfg(all(windows, feature = "namedpipes"))]
|
||||
NamedPipes(namedpipes::PageantStream),
|
||||
}
|
||||
|
||||
impl PageantStream {
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
let mut errors = vec![];
|
||||
// if compiled in, try the more modern named pipes approach first:
|
||||
#[cfg(all(windows, feature = "namedpipes"))]
|
||||
{
|
||||
match namedpipes::PageantStream::new().await {
|
||||
Ok(s) => {
|
||||
return Ok(Self::NamedPipes(s));
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Pageant NamedPipes connection failed: {e}");
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(windows, feature = "wmmessage"))]
|
||||
{
|
||||
match wmmessage::PageantStream::new().await {
|
||||
Ok(s) => {
|
||||
return Ok(Self::WmMessage(s));
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Pageant WM_Message connection failed: {e}");
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.len() == 1
|
||||
&& let Some(err) = errors.pop()
|
||||
{
|
||||
Err(err)
|
||||
} else {
|
||||
Err(Error::Multiple(errors))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for PageantStream {
|
||||
fn poll_read(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
match &mut *self {
|
||||
#[cfg(all(windows, feature = "wmmessage"))]
|
||||
Self::WmMessage(i) => wmmessage::PageantStream::poll_read(Pin::new(i), cx, buf),
|
||||
#[cfg(all(windows, feature = "namedpipes"))]
|
||||
Self::NamedPipes(i) => namedpipes::PageantStream::poll_read(Pin::new(i), cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for PageantStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<Result<usize, std::io::Error>> {
|
||||
match &mut *self {
|
||||
#[cfg(all(windows, feature = "wmmessage"))]
|
||||
Self::WmMessage(i) => wmmessage::PageantStream::poll_write(Pin::new(i), cx, buf),
|
||||
#[cfg(all(windows, feature = "namedpipes"))]
|
||||
Self::NamedPipes(i) => namedpipes::PageantStream::poll_write(Pin::new(i), cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
match &mut *self {
|
||||
#[cfg(all(windows, feature = "wmmessage"))]
|
||||
Self::WmMessage(i) => wmmessage::PageantStream::poll_flush(Pin::new(i), cx),
|
||||
#[cfg(all(windows, feature = "namedpipes"))]
|
||||
Self::NamedPipes(i) => namedpipes::PageantStream::poll_flush(Pin::new(i), cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
match &mut *self {
|
||||
#[cfg(all(windows, feature = "wmmessage"))]
|
||||
Self::WmMessage(i) => wmmessage::PageantStream::poll_shutdown(Pin::new(i), cx),
|
||||
#[cfg(all(windows, feature = "namedpipes"))]
|
||||
Self::NamedPipes(i) => namedpipes::PageantStream::poll_shutdown(Pin::new(i), cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
//! # Pageant SSH agent transport protocol implementation
|
||||
//!
|
||||
//! This crate provides a [PageantStream] type that implements [AsyncRead] and [AsyncWrite] traits and can be used to talk to a running Pageant instance.
|
||||
//!
|
||||
//! This crate only implements the transport, not the actual SSH agent protocol.
|
||||
|
||||
#![deny(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
|
||||
#[cfg(all(windows, target_vendor = "win7"))]
|
||||
pub(crate) use windows_win7 as windows;
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
pub(crate) use windows_modern as windows;
|
||||
|
||||
mod error;
|
||||
pub use error::*;
|
||||
|
||||
#[cfg(all(windows, feature = "wmmessage"))]
|
||||
pub mod wmmessage;
|
||||
|
||||
#[cfg(all(windows, feature = "namedpipes"))]
|
||||
pub mod namedpipes;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod interface;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub use interface::*;
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
use std::io::IoSlice;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use base16ct::lower;
|
||||
use delegate::delegate;
|
||||
use log::debug;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient};
|
||||
use crate::windows::Win32::Foundation::ERROR_PIPE_BUSY;
|
||||
use crate::windows::Win32::Security::Authentication::Identity::{GetUserNameExA, NameUserPrincipal};
|
||||
use crate::windows::Win32::Security::Cryptography::{
|
||||
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_CROSS_PROCESS, CryptProtectMemory,
|
||||
};
|
||||
use crate::windows::core::PSTR;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// Pageant transport stream. Implements [AsyncRead] and [AsyncWrite].
|
||||
pub struct PageantStream {
|
||||
stream: NamedPipeClient,
|
||||
}
|
||||
|
||||
impl PageantStream {
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
let pipe_name = Self::determine_pipe_name()?;
|
||||
debug!("Opening pipe '{}'", pipe_name);
|
||||
let mut timeout_counter = 0;
|
||||
let stream = loop {
|
||||
match ClientOptions::new().open(&pipe_name) {
|
||||
Ok(client) => break client,
|
||||
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
timeout_counter += 1;
|
||||
if timeout_counter > 40 {
|
||||
return Err(Error::PipeBusy);
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
};
|
||||
|
||||
Ok(Self { stream })
|
||||
}
|
||||
|
||||
fn determine_pipe_name() -> Result<String, Error> {
|
||||
let username = Self::get_username()?;
|
||||
let suffix = Self::capi_obfuscate_string("Pageant")?;
|
||||
Ok(format!("\\\\.\\pipe\\pageant.{username}.{suffix}"))
|
||||
}
|
||||
|
||||
fn get_username() -> Result<String, Error> {
|
||||
unsafe {
|
||||
let mut name_length = 0;
|
||||
|
||||
// don't check result on this, always returns ERROR_MORE_DATA
|
||||
GetUserNameExA(NameUserPrincipal, None, &mut name_length);
|
||||
|
||||
let mut name_buf = vec![0u8; name_length as usize];
|
||||
|
||||
if !GetUserNameExA(
|
||||
NameUserPrincipal,
|
||||
Some(PSTR(name_buf.as_mut_ptr())),
|
||||
&mut name_length,
|
||||
) {
|
||||
// Pageant falls back to GetUserNameA here,
|
||||
// but as far as I can tell, all Versions of Windows supported by Rust today
|
||||
// should be able to answer the UserNameEx request - the comments in Pageant source
|
||||
// point to Windows XP and earlier compatibility...
|
||||
return Err(Error::from_win32());
|
||||
}
|
||||
|
||||
//remove terminating null
|
||||
if let Some(0) = name_buf.pop() {
|
||||
let mut name = String::from_utf8(name_buf).map_err(|_| Error::InvalidUsername)?;
|
||||
if let Some(at_index) = name.find('@') {
|
||||
name.drain(at_index..);
|
||||
}
|
||||
Ok(name)
|
||||
} else {
|
||||
Err(Error::InvalidUsername)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn capi_obfuscate_string(input: &str) -> Result<String, Error> {
|
||||
let mut cryptlen = input.len() + 1;
|
||||
cryptlen = cryptlen.next_multiple_of(CRYPTPROTECTMEMORY_BLOCK_SIZE as usize);
|
||||
let mut cryptdata = vec![0u8; cryptlen];
|
||||
|
||||
// copy cleartext into crypt buffer:
|
||||
cryptdata
|
||||
.iter_mut()
|
||||
.zip(input.as_bytes())
|
||||
.for_each(|(c, i)| *c = *i);
|
||||
// (since the buffer is initialized to 0 and always at least 1 longer than the input,
|
||||
// we don't need to worry about terminating the string)
|
||||
|
||||
unsafe {
|
||||
// Errors are explicitly ignored:
|
||||
let _ = CryptProtectMemory(
|
||||
cryptdata.as_mut_ptr() as *mut _,
|
||||
cryptlen as u32,
|
||||
CRYPTPROTECTMEMORY_CROSS_PROCESS,
|
||||
);
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update((cryptdata.len() as u32).to_be_bytes());
|
||||
hasher.update(&cryptdata);
|
||||
Ok(lower::encode_string(&hasher.finalize()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for PageantStream {
|
||||
delegate! {
|
||||
to Pin::new(&mut self.stream) {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>>;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for PageantStream {
|
||||
delegate! {
|
||||
to Pin::new(&mut self.stream) {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>>;
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
|
||||
|
||||
fn poll_write_vectored(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[IoSlice<'_>],
|
||||
) -> Poll<Result<usize, std::io::Error>>;
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
|
||||
}
|
||||
|
||||
to Pin::new(&self.stream) {
|
||||
fn is_write_vectored(&self) -> bool;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
use std::ffi::CString;
|
||||
use std::io::IoSlice;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use bytes::BytesMut;
|
||||
use delegate::delegate;
|
||||
use log::debug;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf};
|
||||
use crate::windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, INVALID_HANDLE_VALUE, LPARAM, WPARAM};
|
||||
use crate::windows::Win32::Security::{
|
||||
GetTokenInformation, InitializeSecurityDescriptor, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES,
|
||||
SECURITY_DESCRIPTOR, SetSecurityDescriptorOwner, TOKEN_QUERY, TOKEN_USER, TokenUser,
|
||||
};
|
||||
use crate::windows::Win32::System::DataExchange::COPYDATASTRUCT;
|
||||
use crate::windows::Win32::System::Memory::{
|
||||
CreateFileMappingW, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, PAGE_READWRITE,
|
||||
UnmapViewOfFile,
|
||||
};
|
||||
use crate::windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
||||
use crate::windows::Win32::UI::WindowsAndMessaging::{FindWindowW, SendMessageA, WM_COPYDATA};
|
||||
use crate::windows::core::PCWSTR;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// Pageant transport stream. Implements [AsyncRead] and [AsyncWrite].
|
||||
///
|
||||
/// The stream has a unique cookie and requests made in the same stream are considered the same "session".
|
||||
pub struct PageantStream {
|
||||
stream: DuplexStream,
|
||||
}
|
||||
|
||||
impl PageantStream {
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
let (one, mut two) = tokio::io::duplex(_AGENT_MAX_MSGLEN * 100);
|
||||
|
||||
let cookie = rand::random::<u64>().to_string();
|
||||
tokio::spawn(async move {
|
||||
let mut buf = BytesMut::new();
|
||||
while let Ok(n) = two.read_buf(&mut buf).await {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
if buf.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
let len = BigEndian::read_u32(&buf) as usize;
|
||||
if buf.len() < len + 4 {
|
||||
continue;
|
||||
}
|
||||
let msg = buf.split_to(len + 4).freeze();
|
||||
let Ok(response) = query_pageant_direct(cookie.clone(), &msg).map_err(|e| {
|
||||
debug!("Pageant query failed: {:?}", e);
|
||||
e
|
||||
}) else {
|
||||
break;
|
||||
};
|
||||
two.write_all(&response).await?
|
||||
}
|
||||
std::io::Result::Ok(())
|
||||
});
|
||||
|
||||
Ok(Self { stream: one })
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for PageantStream {
|
||||
delegate! {
|
||||
to Pin::new(&mut self.stream) {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>>;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for PageantStream {
|
||||
delegate! {
|
||||
to Pin::new(&mut self.stream) {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>>;
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
|
||||
|
||||
fn poll_write_vectored(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[IoSlice<'_>],
|
||||
) -> Poll<Result<usize, std::io::Error>>;
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
|
||||
}
|
||||
|
||||
to Pin::new(&self.stream) {
|
||||
fn is_write_vectored(&self) -> bool;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MemoryMap {
|
||||
filemap: HANDLE,
|
||||
view: MEMORY_MAPPED_VIEW_ADDRESS,
|
||||
length: usize,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl MemoryMap {
|
||||
fn new(
|
||||
name: String,
|
||||
length: usize,
|
||||
security_attributes: Option<SECURITY_ATTRIBUTES>,
|
||||
) -> Result<Self, Error> {
|
||||
let wide_name = wide_null(&name);
|
||||
let filemap = unsafe {
|
||||
CreateFileMappingW(
|
||||
INVALID_HANDLE_VALUE,
|
||||
security_attributes.map(|sa| &sa as *const _),
|
||||
PAGE_READWRITE,
|
||||
0,
|
||||
length as u32,
|
||||
PCWSTR(wide_name.as_ptr()),
|
||||
)
|
||||
}?;
|
||||
if filemap.is_invalid() {
|
||||
return Err(Error::from_win32());
|
||||
}
|
||||
let view = unsafe { MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) };
|
||||
Ok(Self {
|
||||
filemap,
|
||||
view,
|
||||
length,
|
||||
pos: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn seek(&mut self, pos: usize) {
|
||||
self.pos = pos;
|
||||
}
|
||||
|
||||
fn write(&mut self, data: &[u8]) -> Result<(), Error> {
|
||||
if self.pos + data.len() > self.length {
|
||||
return Err(Error::Overflow);
|
||||
}
|
||||
|
||||
if data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
unsafe {
|
||||
#[allow(clippy::indexing_slicing)] // length checked
|
||||
std::ptr::copy_nonoverlapping(
|
||||
&data[0] as *const u8,
|
||||
self.view.Value.add(self.pos) as *mut u8,
|
||||
data.len(),
|
||||
);
|
||||
}
|
||||
self.pos += data.len();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(&mut self, n: usize) -> Vec<u8> {
|
||||
let out = vec![0; n];
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
self.view.Value.add(self.pos) as *const u8,
|
||||
out.as_ptr() as *mut u8,
|
||||
n,
|
||||
);
|
||||
}
|
||||
self.pos += n;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MemoryMap {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = UnmapViewOfFile(self.view);
|
||||
let _ = CloseHandle(self.filemap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_pageant_window() -> Result<HWND, Error> {
|
||||
let pageant = wide_null("Pageant");
|
||||
let w = unsafe { FindWindowW(PCWSTR(pageant.as_ptr()), PCWSTR(pageant.as_ptr())) }?;
|
||||
if w.is_invalid() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
Ok(w)
|
||||
}
|
||||
|
||||
fn wide_null(value: &str) -> Vec<u16> {
|
||||
value.encode_utf16().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
const _AGENT_COPYDATA_ID: u64 = 0x804E50BA;
|
||||
const _AGENT_MAX_MSGLEN: usize = 8192;
|
||||
|
||||
pub fn is_pageant_running() -> bool {
|
||||
find_pageant_window().is_ok()
|
||||
}
|
||||
|
||||
fn get_current_process_user() -> Result<TOKEN_USER, Error> {
|
||||
unsafe {
|
||||
let mut process_token = HANDLE::default();
|
||||
OpenProcessToken(
|
||||
GetCurrentProcess(),
|
||||
TOKEN_QUERY,
|
||||
&mut process_token as *mut _,
|
||||
)?;
|
||||
|
||||
let mut info_size = 0;
|
||||
let _ = GetTokenInformation(process_token, TokenUser, None, 0, &mut info_size);
|
||||
|
||||
let mut buffer = vec![0; info_size as usize];
|
||||
GetTokenInformation(
|
||||
process_token,
|
||||
TokenUser,
|
||||
Some(buffer.as_mut_ptr() as *mut _),
|
||||
buffer.len() as u32,
|
||||
&mut info_size,
|
||||
)?;
|
||||
let user: TOKEN_USER = *(buffer.as_ptr() as *const _);
|
||||
let _ = CloseHandle(process_token);
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a one-off query to Pageant and return a response.
|
||||
pub fn query_pageant_direct(cookie: String, msg: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let hwnd = find_pageant_window()?;
|
||||
let map_name = format!("PageantRequest{cookie}");
|
||||
|
||||
let user = get_current_process_user()?;
|
||||
|
||||
let mut sd = SECURITY_DESCRIPTOR::default();
|
||||
let sa = SECURITY_ATTRIBUTES {
|
||||
lpSecurityDescriptor: &mut sd as *mut _ as *mut _,
|
||||
bInheritHandle: true.into(),
|
||||
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
};
|
||||
|
||||
let psd = PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _);
|
||||
|
||||
unsafe {
|
||||
InitializeSecurityDescriptor(psd, 1)?;
|
||||
SetSecurityDescriptorOwner(psd, Some(user.User.Sid), false)?;
|
||||
}
|
||||
|
||||
let mut map: MemoryMap = MemoryMap::new(map_name.clone(), _AGENT_MAX_MSGLEN, Some(sa))?;
|
||||
map.write(msg)?;
|
||||
|
||||
let char_buffer = CString::new(map_name.as_bytes()).map_err(|_| Error::InvalidCookie)?;
|
||||
let cds = COPYDATASTRUCT {
|
||||
dwData: _AGENT_COPYDATA_ID as usize,
|
||||
cbData: char_buffer.as_bytes().len() as u32,
|
||||
lpData: char_buffer.as_bytes().as_ptr() as *mut _,
|
||||
};
|
||||
|
||||
let response = unsafe {
|
||||
SendMessageA(
|
||||
hwnd,
|
||||
WM_COPYDATA,
|
||||
WPARAM(0), // Should be window handle to requesting app, which we don't have
|
||||
LPARAM(&cds as *const _ as isize),
|
||||
)
|
||||
};
|
||||
|
||||
if response.0 == 0 {
|
||||
return Err(Error::NoResponse);
|
||||
}
|
||||
|
||||
map.seek(0);
|
||||
let mut buf = map.read(4);
|
||||
if buf.len() < 4 {
|
||||
return Err(Error::NoResponse);
|
||||
}
|
||||
#[allow(clippy::indexing_slicing)] // length checked
|
||||
let size = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
|
||||
buf.extend(map.read(size));
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
name = "wry"
|
||||
version = "0.55.1"
|
||||
authors = ["Tauri Programme within The Commons Conservancy"]
|
||||
build = "build.rs"
|
||||
exclude = [
|
||||
"/.changes",
|
||||
"/.github",
|
||||
"/audits",
|
||||
"/wry-logo.svg",
|
||||
]
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Cross-platform WebView rendering library"
|
||||
documentation = "https://docs.rs/wry"
|
||||
readme = "README.md"
|
||||
categories = ["gui"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/tauri-apps/wry"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
no-default-features = true
|
||||
features = [
|
||||
"protocol",
|
||||
"os-webview",
|
||||
]
|
||||
targets = [
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-apple-darwin",
|
||||
]
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"protocol",
|
||||
"os-webview",
|
||||
"x11",
|
||||
]
|
||||
devtools = []
|
||||
fullscreen = []
|
||||
linux-body = [
|
||||
"webkit2gtk/v2_40",
|
||||
"os-webview",
|
||||
]
|
||||
mac-proxy = []
|
||||
os-webview = [
|
||||
"javascriptcore-rs",
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"dep:gtk",
|
||||
"soup3",
|
||||
]
|
||||
protocol = []
|
||||
serde = ["dpi/serde"]
|
||||
tracing = ["dep:tracing"]
|
||||
transparent = []
|
||||
x11 = [
|
||||
"x11-dl",
|
||||
"gdkx11",
|
||||
"tao/x11",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "wry"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "async_custom_protocol"
|
||||
path = "examples/async_custom_protocol.rs"
|
||||
|
||||
[[example]]
|
||||
name = "cookies"
|
||||
path = "examples/cookies.rs"
|
||||
|
||||
[[example]]
|
||||
name = "custom_protocol"
|
||||
path = "examples/custom_protocol.rs"
|
||||
|
||||
[[example]]
|
||||
name = "custom_titlebar"
|
||||
path = "examples/custom_titlebar.rs"
|
||||
|
||||
[[example]]
|
||||
name = "gtk_multiwebview"
|
||||
path = "examples/gtk_multiwebview.rs"
|
||||
|
||||
[[example]]
|
||||
name = "gtk_opengl"
|
||||
path = "examples/gtk_opengl.rs"
|
||||
|
||||
[[example]]
|
||||
name = "multiwebview"
|
||||
path = "examples/multiwebview.rs"
|
||||
|
||||
[[example]]
|
||||
name = "multiwindow"
|
||||
path = "examples/multiwindow.rs"
|
||||
|
||||
[[example]]
|
||||
name = "reparent"
|
||||
path = "examples/reparent.rs"
|
||||
|
||||
[[example]]
|
||||
name = "simple"
|
||||
path = "examples/simple.rs"
|
||||
|
||||
[[example]]
|
||||
name = "streaming"
|
||||
path = "examples/streaming.rs"
|
||||
|
||||
[[example]]
|
||||
name = "transparent"
|
||||
path = "examples/transparent.rs"
|
||||
|
||||
[[example]]
|
||||
name = "wgpu"
|
||||
path = "examples/wgpu.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_border"
|
||||
path = "examples/window_border.rs"
|
||||
|
||||
[[example]]
|
||||
name = "winit"
|
||||
path = "examples/winit.rs"
|
||||
|
||||
[dependencies.cookie]
|
||||
version = "0.18"
|
||||
|
||||
[dependencies.dpi]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.http]
|
||||
version = "1.1"
|
||||
|
||||
[dependencies.once_cell]
|
||||
version = "1"
|
||||
|
||||
[dependencies.raw-window-handle]
|
||||
version = "0.6"
|
||||
features = ["std"]
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2.0"
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1"
|
||||
optional = true
|
||||
|
||||
[dev-dependencies.base64]
|
||||
version = "0.22"
|
||||
|
||||
[dev-dependencies.dom_query]
|
||||
version = "0.27.0"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.getrandom]
|
||||
version = "0.3"
|
||||
|
||||
[dev-dependencies.glow]
|
||||
version = "0.16.0"
|
||||
|
||||
[dev-dependencies.http-range]
|
||||
version = "0.1"
|
||||
|
||||
[dev-dependencies.libloading]
|
||||
version = "0.8.9"
|
||||
|
||||
[dev-dependencies.percent-encoding]
|
||||
version = "2.3"
|
||||
|
||||
[dev-dependencies.pollster]
|
||||
version = "0.4.0"
|
||||
|
||||
[dev-dependencies.sha2]
|
||||
version = "0.10"
|
||||
|
||||
[dev-dependencies.tao]
|
||||
version = "0.35"
|
||||
|
||||
[dev-dependencies.wgpu]
|
||||
version = "23"
|
||||
|
||||
[dev-dependencies.winit]
|
||||
version = "0.30"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.dirs]
|
||||
version = "6"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gdkx11]
|
||||
version = "0.18"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gtk]
|
||||
version = "0.18"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.javascriptcore-rs]
|
||||
version = "=1.1.2"
|
||||
features = ["v2_28"]
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.percent-encoding]
|
||||
version = "2.3"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.soup3]
|
||||
version = "0.5"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk]
|
||||
version = "=2.0.2"
|
||||
features = ["v2_38"]
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk-sys]
|
||||
version = "=2.0.2"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.x11-dl]
|
||||
version = "2.21"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dev-dependencies.x11-dl]
|
||||
version = "2.21"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.base64]
|
||||
version = "0.22"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.crossbeam-channel]
|
||||
version = "0.5"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.dom_query]
|
||||
version = "0.27.0"
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.jni]
|
||||
version = "0.21"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.libc]
|
||||
version = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.ndk]
|
||||
version = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.sha2]
|
||||
version = "0.10"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.tao-macros]
|
||||
version = "0.1"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies.objc2-ui-kit]
|
||||
version = "0.3.0"
|
||||
features = [
|
||||
"std",
|
||||
"objc2-core-foundation",
|
||||
"UIResponder",
|
||||
"UIScrollView",
|
||||
"UIView",
|
||||
"UIWindow",
|
||||
"UIApplication",
|
||||
"UIEvent",
|
||||
"UIColor",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit]
|
||||
version = "0.3.0"
|
||||
features = [
|
||||
"std",
|
||||
"objc2-core-foundation",
|
||||
"NSApplication",
|
||||
"NSButton",
|
||||
"NSControl",
|
||||
"NSEvent",
|
||||
"NSWindow",
|
||||
"NSView",
|
||||
"NSPasteboard",
|
||||
"NSPanel",
|
||||
"NSResponder",
|
||||
"NSOpenPanel",
|
||||
"NSSavePanel",
|
||||
"NSMenu",
|
||||
"NSGraphics",
|
||||
"NSScreen",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.dunce]
|
||||
version = "1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.webview2-com]
|
||||
version = "0.38"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
||||
version = "0.61"
|
||||
features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_Globalization",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
]
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows-core]
|
||||
version = "0.61"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows-version]
|
||||
version = "0.1"
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies.block2]
|
||||
version = "0.6"
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies.dirs]
|
||||
version = "6"
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies.objc2]
|
||||
version = "0.6.4"
|
||||
features = [
|
||||
"exception",
|
||||
"disable-encoding-assertions",
|
||||
]
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-core-foundation]
|
||||
version = "0.3.0"
|
||||
features = [
|
||||
"std",
|
||||
"CFCGTypes",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-foundation]
|
||||
version = "0.3.0"
|
||||
features = [
|
||||
"std",
|
||||
"objc2-core-foundation",
|
||||
"NSURLRequest",
|
||||
"NSURL",
|
||||
"NSString",
|
||||
"NSKeyValueCoding",
|
||||
"NSStream",
|
||||
"NSDictionary",
|
||||
"NSObject",
|
||||
"NSData",
|
||||
"NSEnumerator",
|
||||
"NSKeyValueObserving",
|
||||
"NSThread",
|
||||
"NSJSONSerialization",
|
||||
"NSDate",
|
||||
"NSBundle",
|
||||
"NSProcessInfo",
|
||||
"NSValue",
|
||||
"NSRange",
|
||||
"NSRunLoop",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-web-kit]
|
||||
version = "0.3.0"
|
||||
features = [
|
||||
"std",
|
||||
"objc2-core-foundation",
|
||||
"objc2-app-kit",
|
||||
"block2",
|
||||
"WKWebView",
|
||||
"WKWebViewConfiguration",
|
||||
"WKWebsiteDataStore",
|
||||
"WKDownload",
|
||||
"WKDownloadDelegate",
|
||||
"WKNavigation",
|
||||
"WKNavigationDelegate",
|
||||
"WKUserContentController",
|
||||
"WKURLSchemeHandler",
|
||||
"WKPreferences",
|
||||
"WKURLSchemeTask",
|
||||
"WKScriptMessageHandler",
|
||||
"WKUIDelegate",
|
||||
"WKOpenPanelParameters",
|
||||
"WKFrameInfo",
|
||||
"WKSecurityOrigin",
|
||||
"WKScriptMessage",
|
||||
"WKNavigationAction",
|
||||
"WKWebpagePreferences",
|
||||
"WKNavigationResponse",
|
||||
"WKUserScript",
|
||||
"WKHTTPCookieStore",
|
||||
"WKWindowFeatures",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies.url]
|
||||
version = "2.5"
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "warn"
|
||||
priority = 0
|
||||
check-cfg = [
|
||||
"cfg(linux)",
|
||||
"cfg(gtk)",
|
||||
]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# DBX patch
|
||||
|
||||
DBX vendors Wry 0.55.1 to pass `WEBVIEW2_BROWSER_EXECUTABLE_FOLDER` directly
|
||||
to both WebView2 Runtime discovery and environment creation on the Win7 target.
|
||||
|
||||
Upstream Wry passes a null browser folder to these APIs. That works with the
|
||||
Evergreen Runtime but prevents DBX's Windows 7 build from reliably selecting
|
||||
its bundled WebView2 109 Fixed Runtime. Other Windows targets retain the
|
||||
upstream null-folder behavior.
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 Ngo Iok Ui & Tauri Programme within The Commons Conservancy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
SPDXVersion: SPDX-2.1
|
||||
DataLicense: CC0-1.0
|
||||
PackageName: wry
|
||||
DataFormat: SPDXRef-1
|
||||
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||
PackageHomePage: https://tauri.app
|
||||
PackageLicenseDeclared: Apache-2.0
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: 2020-2023, The Tauri Programme in the Commons Conservancy
|
||||
PackageSummary: <text>Wry is the official, rust-based webview
|
||||
windowing service for Tauri.
|
||||
</text>
|
||||
PackageComment: <text>The package includes the following libraries; see
|
||||
Relationship information.
|
||||
</text>
|
||||
Created: 2020-05-20T09:00:00Z
|
||||
PackageDownloadLocation: git://github.com/tauri-apps/wry
|
||||
PackageDownloadLocation: git+https://github.com/tauri-apps/wry.git
|
||||
PackageDownloadLocation: git+ssh://github.com/tauri-apps/wry.git
|
||||
Creator: Person: Daniel Thompson-Yvetot
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
<p align="center"><img height="100" src="https://raw.githubusercontent.com/tauri-apps/wry/refs/heads/dev/.github/splash.png" alt="WRY Webview Rendering library" /></p>
|
||||
|
||||
[](https://crates.io/crates/wry) [](https://docs.rs/wry/)
|
||||
[](https://opencollective.com/tauri)
|
||||
[](https://discord.gg/SpmNs4S)
|
||||
[](https://tauri.app)
|
||||
[](https://good-labs.github.io/greater-good-affirmation)
|
||||
[](https://opencollective.com/tauri)
|
||||
|
||||
Wry is a cross-platform WebView rendering library.
|
||||
|
||||
The webview requires a running event loop and a window type that implements [`HasWindowHandle`],
|
||||
or a gtk container widget if you need to support X11 and Wayland.
|
||||
You can use a windowing library like [`tao`] or [`winit`].
|
||||
|
||||
### Examples
|
||||
|
||||
This example leverages the [`HasWindowHandle`] and supports Windows, macOS, iOS, Android and Linux (X11 Only).
|
||||
See the following example using [`winit`]:
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
window: Option<Window>,
|
||||
webview: Option<wry::WebView>,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let window = event_loop.create_window(Window::default_attributes()).unwrap();
|
||||
let webview = WebViewBuilder::new()
|
||||
.with_url("https://tauri.app")
|
||||
.build(&window)
|
||||
.unwrap();
|
||||
|
||||
self.window = Some(window);
|
||||
self.webview = Some(webview);
|
||||
}
|
||||
|
||||
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
|
||||
}
|
||||
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let mut app = App::default();
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
```
|
||||
|
||||
If you also want to support Wayland too, then we recommend you use [`WebViewBuilderExtUnix::new_gtk`] on Linux.
|
||||
See the following example using [`tao`]:
|
||||
|
||||
```rust
|
||||
let event_loop = EventLoop::new();
|
||||
let window = WindowBuilder::new().build(&event_loop).unwrap();
|
||||
|
||||
let builder = WebViewBuilder::new().with_url("https://tauri.app");
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let webview = builder.build(&window).unwrap();
|
||||
#[cfg(target_os = "linux")]
|
||||
let webview = builder.build_gtk(window.gtk_window()).unwrap();
|
||||
```
|
||||
|
||||
### Child webviews
|
||||
|
||||
You can use [`WebViewBuilder::build_as_child`] to create the webview as a child inside another window. This is supported on
|
||||
macOS, Windows and Linux (X11 Only).
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
window: Option<Window>,
|
||||
webview: Option<wry::WebView>,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let window = event_loop.create_window(Window::default_attributes()).unwrap();
|
||||
let webview = WebViewBuilder::new()
|
||||
.with_url("https://tauri.app")
|
||||
.with_bounds(Rect {
|
||||
position: LogicalPosition::new(100, 100).into(),
|
||||
size: LogicalSize::new(200, 200).into(),
|
||||
})
|
||||
.build_as_child(&window)
|
||||
.unwrap();
|
||||
|
||||
self.window = Some(window);
|
||||
self.webview = Some(webview);
|
||||
}
|
||||
|
||||
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
|
||||
}
|
||||
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let mut app = App::default();
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
```
|
||||
|
||||
If you want to support X11 and Wayland at the same time, we recommend using
|
||||
[`WebViewExtUnix::new_gtk`] or [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`].
|
||||
|
||||
```rust
|
||||
let event_loop = EventLoop::new();
|
||||
let window = WindowBuilder::new().build(&event_loop).unwrap();
|
||||
|
||||
let builder = WebViewBuilder::new()
|
||||
.with_url("https://tauri.app")
|
||||
.with_bounds(Rect {
|
||||
position: LogicalPosition::new(100, 100).into(),
|
||||
size: LogicalSize::new(200, 200).into(),
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let webview = builder.build_as_child(&window).unwrap();
|
||||
#[cfg(target_os = "linux")]
|
||||
let webview = {
|
||||
# use gtk::prelude::*;
|
||||
let vbox = window.default_vbox().unwrap(); // tao adds a gtk::Box by default
|
||||
let fixed = gtk::Fixed::new();
|
||||
fixed.show_all();
|
||||
vbox.pack_start(&fixed, true, true, 0);
|
||||
builder.build_gtk(&fixed).unwrap()
|
||||
};
|
||||
```
|
||||
|
||||
### Platform Considerations
|
||||
|
||||
Here is the underlying web engine each platform uses, and some dependencies you might need to install.
|
||||
|
||||
#### Linux
|
||||
|
||||
[WebKitGTK](https://webkitgtk.org/) is used to provide webviews on Linux which requires GTK,
|
||||
so if the windowing library doesn't support GTK (as in [`winit`])
|
||||
you'll need to call [`gtk::init`] before creating the webview and then call [`gtk::main_iteration_do`] alongside
|
||||
your windowing library event loop.
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
webview_window: Option<(Window, WebView)>,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let window = event_loop.create_window(Window::default_attributes()).unwrap();
|
||||
let webview = WebViewBuilder::new()
|
||||
.with_url("https://tauri.app")
|
||||
.build(&window)
|
||||
.unwrap();
|
||||
|
||||
self.webview_window = Some((window, webview));
|
||||
}
|
||||
|
||||
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
|
||||
|
||||
// Advance GTK event loop <!----- IMPORTANT
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
#[cfg(target_os = "linux")]
|
||||
while gtk::events_pending() {
|
||||
gtk::main_iteration_do(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let mut app = App::default();
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
```
|
||||
|
||||
##### Linux Dependencies
|
||||
|
||||
###### Arch Linux / Manjaro:
|
||||
|
||||
```bash
|
||||
sudo pacman -S webkit2gtk-4.1
|
||||
```
|
||||
|
||||
###### Debian / Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt install libwebkit2gtk-4.1-dev
|
||||
```
|
||||
|
||||
###### Fedora
|
||||
|
||||
```bash
|
||||
sudo dnf install gtk3-devel webkit2gtk4.1-devel
|
||||
```
|
||||
|
||||
###### Nix & NixOS
|
||||
|
||||
```nix
|
||||
# shell.nix
|
||||
|
||||
let
|
||||
# Unstable Channel | Rolling Release
|
||||
pkgs = import (fetchTarball("channel:nixpkgs-unstable")) { };
|
||||
packages = with pkgs; [
|
||||
pkg-config
|
||||
webkitgtk_4_1
|
||||
];
|
||||
in
|
||||
pkgs.mkShell {
|
||||
buildInputs = packages;
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
nix-shell shell.nix
|
||||
```
|
||||
|
||||
###### GUIX
|
||||
|
||||
```scheme
|
||||
;; manifest.scm
|
||||
|
||||
(specifications->manifest
|
||||
'("pkg-config" ; Helper tool used when compiling
|
||||
"webkitgtk" ; Web content engine fot GTK+
|
||||
))
|
||||
```
|
||||
|
||||
```bash
|
||||
guix shell -m manifest.scm
|
||||
```
|
||||
|
||||
#### macOS
|
||||
|
||||
WebKit is native on macOS so everything should be fine.
|
||||
|
||||
If you are cross-compiling for macOS using [osxcross](https://github.com/tpoechtrager/osxcross) and encounter a runtime panic like `Class with name WKWebViewConfiguration could not be found` it's possible that `WebKit.framework` has not been linked correctly, to fix this set the `RUSTFLAGS` environment variable:
|
||||
|
||||
```bash
|
||||
RUSTFLAGS="-l framework=WebKit" cargo build --target=x86_64-apple-darwin --release
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
WebView2 provided by Microsoft Edge Chromium is used. So wry supports Windows 7, 8, 10 and 11.
|
||||
|
||||
#### Android
|
||||
|
||||
In order for `wry` to be able to create webviews on Android, there are a few requirements that your application needs to uphold:
|
||||
|
||||
1. You need to set a few environment variables that will be used to generate the necessary kotlin
|
||||
files that you need to include in your Android application for wry to function properly.
|
||||
- `WRY_ANDROID_PACKAGE`: which is the reversed domain name of your android project and the app name in snake_case, for example, `com.wry.example.wry_app`
|
||||
- `WRY_ANDROID_LIBRARY`: for example, if your cargo project has a lib name `wry_app`, it will generate `libwry_app.so` so you set this env var to `wry_app`
|
||||
- `WRY_ANDROID_KOTLIN_FILES_OUT_DIR`: for example, `path/to/app/src/main/kotlin/com/wry/example`
|
||||
2. Your main Android Activity needs to inherit `AppCompatActivity`, preferably it should use the generated `WryActivity` or inherit it.
|
||||
3. Your Rust app needs to call `wry::android_setup` function to setup the necessary logic to be able to create webviews later on.
|
||||
4. Your Rust app needs to call `wry::android_binding!` macro to setup the JNI functions that will be called by `WryActivity` and various other places.
|
||||
|
||||
It is recommended to use the [`tao`](https://docs.rs/tao/latest/tao/) crate as it provides maximum compatibility with `wry`.
|
||||
|
||||
```rust
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
tao::android_binding!(
|
||||
com_example,
|
||||
wry_app,
|
||||
WryActivity,
|
||||
wry::android_setup, // pass the wry::android_setup function to tao which will be invoked when the event loop is created
|
||||
_start_app
|
||||
);
|
||||
wry::android_binding!(com_example, ttt);
|
||||
}
|
||||
```
|
||||
|
||||
If this feels overwhelming, you can just use the preconfigured template from [`cargo-mobile2`](https://github.com/tauri-apps/cargo-mobile2).
|
||||
|
||||
For more information, check out [MOBILE.md](https://github.com/tauri-apps/wry/blob/dev/MOBILE.md).
|
||||
|
||||
### Feature flags
|
||||
|
||||
Wry uses a set of feature flags to toggle several advanced features.
|
||||
|
||||
- `os-webview` (default): Enables the default WebView framework on the platform. This must be enabled
|
||||
for the crate to work. This feature was added in preparation of other ports like cef and servo.
|
||||
- `protocol` (default): Enables [`WebViewBuilder::with_custom_protocol`] to define custom URL scheme for handling tasks like
|
||||
loading assets.
|
||||
- `drag-drop` (default): Enables [`WebViewBuilder::with_drag_drop_handler`] to control the behavior when there are files
|
||||
interacting with the window.
|
||||
- `devtools`: Enables devtools on release builds. Devtools are always enabled in debug builds.
|
||||
On **macOS**, enabling devtools, requires calling private APIs so you should not enable this flag in release
|
||||
build if your app needs to publish to App Store.
|
||||
- `transparent`: Transparent background on **macOS** requires calling private functions.
|
||||
Avoid this in release build if your app needs to publish to App Store.
|
||||
- `fullscreen`: Fullscreen video and other media on **macOS** requires calling private functions.
|
||||
Avoid this in release build if your app needs to publish to App Store.
|
||||
- `linux-body`: Enables body support of custom protocol request on Linux. Requires
|
||||
WebKit2GTK v2.40 or above.
|
||||
- `tracing`: enables [`tracing`] for `evaluate_script`, `ipc_handler`, and `custom_protocols`.
|
||||
|
||||
### Partners
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://crabnebula.dev" target="_blank">
|
||||
<img src=".github/sponsors/crabnebula.svg" alt="CrabNebula" width="283">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri).
|
||||
|
||||
### License
|
||||
|
||||
Apache-2.0/MIT
|
||||
|
||||
[`tao`]: https://docs.rs/tao
|
||||
[`winit`]: https://docs.rs/winit
|
||||
[`tracing`]: https://docs.rs/tracing
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
fn main() {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if target_os == "macos" || target_os == "ios" {
|
||||
println!("cargo:rustc-link-lib=framework=WebKit");
|
||||
}
|
||||
|
||||
if target_os == "android" {
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
fn env_var(var: &str) -> String {
|
||||
std::env::var(var).unwrap_or_else(|_| {
|
||||
panic!("`{var}` is not set, which is needed to generate the kotlin files for android.")
|
||||
})
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-env-changed=WRY_ANDROID_PACKAGE");
|
||||
println!("cargo:rerun-if-env-changed=WRY_ANDROID_LIBRARY");
|
||||
println!("cargo:rerun-if-env-changed=WRY_ANDROID_KOTLIN_FILES_OUT_DIR");
|
||||
|
||||
if let Ok(kotlin_out_dir) = std::env::var("WRY_ANDROID_KOTLIN_FILES_OUT_DIR") {
|
||||
let package = env_var("WRY_ANDROID_PACKAGE");
|
||||
let library = env_var("WRY_ANDROID_LIBRARY");
|
||||
|
||||
let kotlin_out_dir = PathBuf::from(&kotlin_out_dir)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(move |_| {
|
||||
panic!("Failed to canonicalize `WRY_ANDROID_KOTLIN_FILES_OUT_DIR` path {kotlin_out_dir}")
|
||||
});
|
||||
|
||||
let kotlin_files_path =
|
||||
PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("src/android/kotlin");
|
||||
println!("cargo:rerun-if-changed={}", kotlin_files_path.display());
|
||||
let kotlin_files = fs::read_dir(kotlin_files_path).expect("failed to read kotlin directory");
|
||||
|
||||
for file in kotlin_files {
|
||||
let file = file.unwrap();
|
||||
|
||||
let class_extension_env = format!(
|
||||
"WRY_{}_CLASS_EXTENSION",
|
||||
file
|
||||
.path()
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_uppercase()
|
||||
);
|
||||
let class_init_env = format!(
|
||||
"WRY_{}_CLASS_INIT",
|
||||
file
|
||||
.path()
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_uppercase()
|
||||
);
|
||||
|
||||
println!("cargo:rerun-if-env-changed={class_extension_env}");
|
||||
println!("cargo:rerun-if-env-changed={class_init_env}");
|
||||
|
||||
let content = fs::read_to_string(file.path())
|
||||
.expect("failed to read kotlin file as string")
|
||||
.replace("{{package}}", &package)
|
||||
.replace("{{package-unescaped}}", &package.replace('`', ""))
|
||||
.replace("{{library}}", &library)
|
||||
.replace(
|
||||
"{{class-extension}}",
|
||||
&std::env::var(&class_extension_env).unwrap_or_default(),
|
||||
)
|
||||
.replace(
|
||||
"{{class-init}}",
|
||||
&std::env::var(&class_init_env).unwrap_or_default(),
|
||||
);
|
||||
|
||||
let auto_generated_comment = match file
|
||||
.path()
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"pro" => "# THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!!\n\n",
|
||||
"kt" => "/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */\n\n",
|
||||
_ => "String::new()",
|
||||
};
|
||||
let mut out = String::from(auto_generated_comment);
|
||||
out.push_str(&content);
|
||||
|
||||
let out_path = kotlin_out_dir.join(file.file_name());
|
||||
// Overwrite only if changed to not trigger rebuilds
|
||||
if fs::read_to_string(&out_path).map_or(true, |o| o != out) {
|
||||
fs::write(&out_path, out).expect("Failed to write kotlin file");
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", out_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
let android = target.contains("android");
|
||||
let linux = !android
|
||||
&& (target.contains("linux")
|
||||
|| target.contains("freebsd")
|
||||
|| target.contains("dragonfly")
|
||||
|| target.contains("netbsd")
|
||||
|| target.contains("openbsd"));
|
||||
alias("linux", linux);
|
||||
alias("gtk", cfg!(feature = "os-webview") && linux);
|
||||
}
|
||||
|
||||
fn alias(alias: &str, condition: bool) {
|
||||
if condition {
|
||||
println!("cargo:rustc-cfg={alias}");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,483 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use http::{
|
||||
header::{HeaderName, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE},
|
||||
Request,
|
||||
};
|
||||
use jni::errors::Result as JniResult;
|
||||
pub use jni::{
|
||||
self,
|
||||
objects::{GlobalRef, JClass, JMap, JObject, JString},
|
||||
sys::{jboolean, jint, jobject, jstring},
|
||||
JNIEnv,
|
||||
};
|
||||
pub use ndk;
|
||||
use ndk::looper::{FdEvent, ThreadLooper};
|
||||
use std::os::fd::{AsFd, AsRawFd};
|
||||
|
||||
use super::{
|
||||
main_pipe::{MainPipe, MAIN_PIPE},
|
||||
ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER,
|
||||
URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
|
||||
};
|
||||
|
||||
use crate::PageLoadEvent;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! android_binding {
|
||||
($domain:ident, $package:ident) => {
|
||||
::wry::android_binding!($domain, $package, ::wry)
|
||||
};
|
||||
// use imported `android_setup` just to force the import path to use `wry::{}`
|
||||
// as the macro breaks without braces
|
||||
($domain:ident, $package:ident, $wry:path) => {{
|
||||
use $wry::{android_setup as _, prelude::*};
|
||||
|
||||
android_fn!($domain, $package, Rust, wryCreate, []);
|
||||
android_fn!(
|
||||
$domain,
|
||||
$package,
|
||||
Rust,
|
||||
onWebviewDestroy,
|
||||
[JObject, JString]
|
||||
);
|
||||
|
||||
android_fn!(
|
||||
$domain,
|
||||
$package,
|
||||
Rust,
|
||||
handleRequest,
|
||||
[JString, JObject, jboolean],
|
||||
jobject
|
||||
);
|
||||
android_fn!(
|
||||
$domain,
|
||||
$package,
|
||||
Rust,
|
||||
withAssetLoader,
|
||||
[JString],
|
||||
jboolean
|
||||
);
|
||||
android_fn!(
|
||||
$domain,
|
||||
$package,
|
||||
Rust,
|
||||
assetLoaderDomain,
|
||||
[JString],
|
||||
jstring
|
||||
);
|
||||
android_fn!(
|
||||
$domain,
|
||||
$package,
|
||||
Rust,
|
||||
shouldOverride,
|
||||
[JString, JString],
|
||||
jboolean
|
||||
);
|
||||
android_fn!($domain, $package, Rust, onEval, [JString, jint, JString]);
|
||||
android_fn!($domain, $package, Rust, onPageLoading, [JString, JString]);
|
||||
android_fn!($domain, $package, Rust, onPageLoaded, [JString, JString]);
|
||||
android_fn!($domain, $package, Rust, ipc, [JString, JString, JString]);
|
||||
android_fn!(
|
||||
$domain,
|
||||
$package,
|
||||
Rust,
|
||||
handleReceivedTitle,
|
||||
[JString, JString],
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
env: &mut JNIEnv,
|
||||
webview_id: JString,
|
||||
request: JObject,
|
||||
is_document_start_script_enabled: jboolean,
|
||||
) -> JniResult<jobject> {
|
||||
let webview_id = env.get_string(&webview_id)?;
|
||||
let webview_id = webview_id.to_str().ok().unwrap_or_default();
|
||||
|
||||
if let Some(handler) = REQUEST_HANDLER.lock().unwrap().get(webview_id) {
|
||||
#[cfg(feature = "tracing")]
|
||||
let span =
|
||||
tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty).entered();
|
||||
|
||||
let mut request_builder = Request::builder();
|
||||
|
||||
let uri = env
|
||||
.call_method(&request, "getUrl", "()Landroid/net/Uri;", &[])?
|
||||
.l()?;
|
||||
let url: JString = env
|
||||
.call_method(&uri, "toString", "()Ljava/lang/String;", &[])?
|
||||
.l()?
|
||||
.into();
|
||||
let url = env.get_string(&url)?.to_string_lossy().to_string();
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
span.record("uri", &url);
|
||||
|
||||
request_builder = request_builder.uri(&url);
|
||||
|
||||
let method = env
|
||||
.call_method(&request, "getMethod", "()Ljava/lang/String;", &[])?
|
||||
.l()
|
||||
.map(JString::from)?;
|
||||
request_builder = request_builder.method(
|
||||
env
|
||||
.get_string(&method)?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let request_headers = env
|
||||
.call_method(request, "getRequestHeaders", "()Ljava/util/Map;", &[])?
|
||||
.l()?;
|
||||
let request_headers = JMap::from_env(env, &request_headers)?;
|
||||
let mut iter = request_headers.iter(env)?;
|
||||
while let Some((header, value)) = iter.next(env)? {
|
||||
let header = JString::from(header);
|
||||
let value = JString::from(value);
|
||||
let header = env.get_string(&header)?;
|
||||
let value = env.get_string(&value)?;
|
||||
if let (Ok(header), Ok(value)) = (
|
||||
HeaderName::from_bytes(header.to_bytes()),
|
||||
HeaderValue::from_bytes(value.to_bytes()),
|
||||
) {
|
||||
request_builder = request_builder.header(header, value);
|
||||
}
|
||||
}
|
||||
|
||||
let final_request = match request_builder.body(Vec::new()) {
|
||||
Ok(req) => req,
|
||||
Err(_e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Failed to build response: {_e}");
|
||||
return Ok(*JObject::null());
|
||||
}
|
||||
};
|
||||
|
||||
let response = {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
|
||||
(handler.handler)(
|
||||
webview_id,
|
||||
final_request,
|
||||
is_document_start_script_enabled != 0,
|
||||
)
|
||||
};
|
||||
if let Some(response) = response {
|
||||
let status = response.status();
|
||||
let status_code = status.as_u16() as i32;
|
||||
let status_err = if status_code < 100 {
|
||||
Some("Status code can't be less than 100")
|
||||
} else if status_code > 599 {
|
||||
Some("statusCode can't be greater than 599.")
|
||||
} else if status_code > 299 && status_code < 400 {
|
||||
Some("statusCode can't be in the [300, 399] range.")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(_err) = status_err {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{_err}");
|
||||
return Ok(*JObject::null());
|
||||
}
|
||||
|
||||
let reason_phrase = status.canonical_reason().unwrap_or("OK");
|
||||
let (mime_type, encoding) = if let Some(content_type) = response.headers().get(CONTENT_TYPE) {
|
||||
let content_type = content_type.to_str().unwrap().trim();
|
||||
let mut s = content_type.split(';');
|
||||
let mime_type = s.next().unwrap().trim();
|
||||
let mut encoding = None;
|
||||
for token in s {
|
||||
let token = token.trim();
|
||||
if token.starts_with("charset=") {
|
||||
encoding.replace(token.split('=').nth(1).unwrap());
|
||||
break;
|
||||
}
|
||||
}
|
||||
(
|
||||
env.new_string(mime_type)?,
|
||||
if let Some(encoding) = encoding {
|
||||
env.new_string(encoding)?
|
||||
} else {
|
||||
JString::default()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(JString::default(), JString::default())
|
||||
};
|
||||
|
||||
let headers = response.headers();
|
||||
let obj = env.new_object("java/util/HashMap", "()V", &[])?;
|
||||
let response_headers = {
|
||||
let headers_map = JMap::from_env(env, &obj)?;
|
||||
for (name, value) in headers.iter() {
|
||||
// WebResourceResponse will automatically generate Content-Type and
|
||||
// Content-Length headers so we should skip them to avoid duplication.
|
||||
if name == CONTENT_TYPE || name == CONTENT_LENGTH {
|
||||
continue;
|
||||
}
|
||||
let key = env.new_string(name)?;
|
||||
let value = env.new_string(value.to_str().unwrap_or_default())?;
|
||||
headers_map.put(env, &key, &value)?;
|
||||
}
|
||||
headers_map
|
||||
};
|
||||
|
||||
let bytes = response.body();
|
||||
|
||||
let byte_array_input_stream = env.find_class("java/io/ByteArrayInputStream")?;
|
||||
let byte_array = env.byte_array_from_slice(bytes)?;
|
||||
let stream = env.new_object(byte_array_input_stream, "([B)V", &[(&byte_array).into()])?;
|
||||
|
||||
let reason_phrase = env.new_string(reason_phrase)?;
|
||||
|
||||
let web_resource_response_class = env.find_class("android/webkit/WebResourceResponse")?;
|
||||
let web_resource_response = env.new_object(
|
||||
web_resource_response_class,
|
||||
"(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/util/Map;Ljava/io/InputStream;)V",
|
||||
&[(&mime_type).into(), (&encoding).into(), status_code.into(), (&reason_phrase).into(), (&response_headers).into(), (&stream).into()],
|
||||
)?;
|
||||
|
||||
return Ok(*web_resource_response);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(*JObject::null())
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn wryCreate(env: JNIEnv, _: JClass) {
|
||||
let mut main_pipe = MainPipe { env };
|
||||
|
||||
let looper = ThreadLooper::for_thread().unwrap();
|
||||
|
||||
looper
|
||||
.add_fd_with_callback(MAIN_PIPE[0].as_fd(), FdEvent::INPUT, move |fd, _event| {
|
||||
let size = std::mem::size_of::<bool>();
|
||||
let mut wake = false;
|
||||
if libc::read(fd.as_raw_fd(), &mut wake as *mut _ as *mut _, size) == size as libc::ssize_t {
|
||||
// unregister itself on errors
|
||||
main_pipe.recv().is_ok()
|
||||
} else {
|
||||
// unregister itself
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn onWebviewDestroy(mut env: JNIEnv, _: JClass, activity: JObject, webview_id: JString) {
|
||||
let activity_id = env
|
||||
.call_method(&activity, "getId", "()I", &[])
|
||||
.unwrap()
|
||||
.i()
|
||||
.unwrap();
|
||||
|
||||
let webview_id = env
|
||||
.get_string(&webview_id)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let is_changing_configurations = env
|
||||
.call_method(&activity, "isChangingConfigurations", "()Z", &[])
|
||||
.unwrap()
|
||||
.z()
|
||||
.unwrap();
|
||||
|
||||
super::MainPipe::send(
|
||||
activity_id,
|
||||
super::WebViewMessage::OnDestroy {
|
||||
activity_id,
|
||||
webview_id,
|
||||
is_changing_configurations,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn handleRequest(
|
||||
mut env: JNIEnv,
|
||||
_: JClass,
|
||||
webview_id: JString,
|
||||
request: JObject,
|
||||
is_document_start_script_enabled: jboolean,
|
||||
) -> jobject {
|
||||
match handle_request(
|
||||
&mut env,
|
||||
webview_id,
|
||||
request,
|
||||
is_document_start_script_enabled,
|
||||
) {
|
||||
Ok(response) => response,
|
||||
Err(_e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Failed to handle request: {_e}");
|
||||
JObject::null().as_raw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn shouldOverride(
|
||||
mut env: JNIEnv,
|
||||
_: JClass,
|
||||
webview_id: JString,
|
||||
url: JString,
|
||||
) -> jboolean {
|
||||
match env.get_string(&url) {
|
||||
Ok(url) => {
|
||||
let url = url.to_string_lossy().to_string();
|
||||
|
||||
let Ok(webview_id) = env.get_string(&webview_id) else {
|
||||
return false.into();
|
||||
};
|
||||
let webview_id = webview_id.to_str().ok().unwrap_or_default();
|
||||
|
||||
URL_LOADING_OVERRIDE
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(webview_id)
|
||||
// We negate the result of the function because the logic for the android
|
||||
// client is different from how the navigation_handler is defined.
|
||||
//
|
||||
// https://developer.android.com/reference/android/webkit/WebViewClient#shouldOverrideUrlLoading(android.webkit.WebView,%20android.webkit.WebResourceRequest)
|
||||
.map(|f| !(f.handler)(url))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Err(_e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Failed to parse JString: {_e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn onEval(mut env: JNIEnv, _: JClass, _webview_id: JString, id: jint, result: JString) {
|
||||
match env.get_string(&result) {
|
||||
Ok(result) => {
|
||||
if let Some(cb) = EVAL_CALLBACKS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&id)
|
||||
{
|
||||
cb(result.into());
|
||||
}
|
||||
}
|
||||
Err(_e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Failed to parse JString: {_e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn ipc(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString, body: JString) {
|
||||
match (
|
||||
env.get_string(&url),
|
||||
env.get_string(&body),
|
||||
env.get_string(&webview_id),
|
||||
) {
|
||||
(Ok(url), Ok(body), Ok(webview_id)) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered();
|
||||
|
||||
let url = url.to_string_lossy().to_string();
|
||||
let body = body.to_string_lossy().to_string();
|
||||
let webview_id = webview_id.to_string_lossy().to_string();
|
||||
if let Some(ipc) = IPC.lock().unwrap().get(&webview_id) {
|
||||
(ipc.handler)(Request::builder().uri(url).body(body).unwrap())
|
||||
}
|
||||
}
|
||||
(Err(_e), _, _) | (_, Err(_e), _) | (_, _, Err(_e)) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Failed to parse JString: {_e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn handleReceivedTitle(mut env: JNIEnv, _: JClass, webview_id: JString, title: JString) {
|
||||
match (env.get_string(&title), env.get_string(&webview_id)) {
|
||||
(Ok(title), Ok(webview_id)) => {
|
||||
let title = title.to_string_lossy().to_string();
|
||||
let webview_id = webview_id.to_string_lossy().to_string();
|
||||
if let Some(title_handler) = TITLE_CHANGE_HANDLER.lock().unwrap().get(&webview_id) {
|
||||
(title_handler.handler)(title)
|
||||
}
|
||||
}
|
||||
(Err(_e), _) | (_, Err(_e)) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Failed to parse JString: {_e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn withAssetLoader(mut env: JNIEnv, _: JClass, webview_id: JString) -> jboolean {
|
||||
let Ok(webview_id) = env.get_string(&webview_id) else {
|
||||
return false.into();
|
||||
};
|
||||
let webview_id = webview_id.to_str().ok().unwrap_or_default();
|
||||
(*WITH_ASSET_LOADER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(webview_id)
|
||||
.unwrap_or(&false))
|
||||
.into()
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn assetLoaderDomain(mut env: JNIEnv, _: JClass, webview_id: JString) -> jstring {
|
||||
let Ok(webview_id) = env.get_string(&webview_id) else {
|
||||
return env.new_string("wry.assets").unwrap().as_raw();
|
||||
};
|
||||
let webview_id = webview_id.to_str().ok().unwrap_or_default();
|
||||
if let Some(domain) = ASSET_LOADER_DOMAIN.lock().unwrap().get(webview_id) {
|
||||
env.new_string(domain).unwrap().as_raw()
|
||||
} else {
|
||||
env.new_string("wry.assets").unwrap().as_raw()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn onPageLoading(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString) {
|
||||
match (env.get_string(&url), env.get_string(&webview_id)) {
|
||||
(Ok(url), Ok(webview_id)) => {
|
||||
let url = url.to_string_lossy().to_string();
|
||||
let webview_id = webview_id.to_string_lossy().to_string();
|
||||
if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().get(&webview_id) {
|
||||
(on_load.handler)(PageLoadEvent::Started, url)
|
||||
}
|
||||
}
|
||||
(Err(_e), _) | (_, Err(_e)) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Failed to parse JString: {_e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString) {
|
||||
match (env.get_string(&url), env.get_string(&webview_id)) {
|
||||
(Ok(url), Ok(webview_id)) => {
|
||||
let url = url.to_string_lossy().to_string();
|
||||
let webview_id = webview_id.to_string_lossy().to_string();
|
||||
if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().get(&webview_id) {
|
||||
(on_load.handler)(PageLoadEvent::Finished, url)
|
||||
}
|
||||
}
|
||||
(Err(_e), _) | (_, Err(_e)) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Failed to parse JString: {_e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package {{package}}
|
||||
|
||||
import android.webkit.*
|
||||
|
||||
class Ipc(val webView: RustWebView, val webViewClient: RustWebViewClient) {
|
||||
@JavascriptInterface
|
||||
fun postMessage(message: String?) {
|
||||
message?.let {m ->
|
||||
// we're not using WebView::getUrl() here because it needs to be executed on the main thread
|
||||
// and it would slow down the Ipc
|
||||
// so instead we track the current URL on the webview client
|
||||
Rust.ipc(webView.id, webViewClient.currentUrl, m)
|
||||
}
|
||||
}
|
||||
|
||||
{{class-extension}}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package {{package}}
|
||||
|
||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
|
||||
class Logger {
|
||||
companion object {
|
||||
private const val LOG_TAG_CORE = "Tauri"
|
||||
|
||||
fun tags(vararg subtags: String): String {
|
||||
return if (subtags.isNotEmpty()) {
|
||||
LOG_TAG_CORE + "/" + TextUtils.join("/", subtags)
|
||||
} else LOG_TAG_CORE
|
||||
}
|
||||
|
||||
fun verbose(message: String) {
|
||||
verbose(LOG_TAG_CORE, message)
|
||||
}
|
||||
|
||||
private fun verbose(tag: String, message: String) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.v(tag, message)
|
||||
}
|
||||
|
||||
fun debug(message: String) {
|
||||
debug(LOG_TAG_CORE, message)
|
||||
}
|
||||
|
||||
fun debug(tag: String, message: String) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.d(tag, message)
|
||||
}
|
||||
|
||||
fun info(message: String) {
|
||||
info(LOG_TAG_CORE, message)
|
||||
}
|
||||
|
||||
fun info(tag: String, message: String) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.i(tag, message)
|
||||
}
|
||||
|
||||
fun warn(message: String) {
|
||||
warn(LOG_TAG_CORE, message)
|
||||
}
|
||||
|
||||
fun warn(tag: String, message: String) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.w(tag, message)
|
||||
}
|
||||
|
||||
fun error(message: String) {
|
||||
error(LOG_TAG_CORE, message, null)
|
||||
}
|
||||
|
||||
fun error(message: String, e: Throwable?) {
|
||||
error(LOG_TAG_CORE, message, e)
|
||||
}
|
||||
|
||||
fun error(tag: String, message: String, e: Throwable?) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.e(tag, message, e)
|
||||
}
|
||||
|
||||
private fun shouldLog(): Boolean {
|
||||
return BuildConfig.DEBUG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package {{package}}
|
||||
|
||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import java.util.ArrayList
|
||||
|
||||
object PermissionHelper {
|
||||
/**
|
||||
* Checks if a list of given permissions are all granted by the user
|
||||
*
|
||||
* @param permissions Permissions to check.
|
||||
* @return True if all permissions are granted, false if at least one is not.
|
||||
*/
|
||||
fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
|
||||
for (perm in permissions) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context!!,
|
||||
perm
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given permission has been defined in the AndroidManifest.xml
|
||||
*
|
||||
* @param permission A permission to check.
|
||||
* @return True if the permission has been defined in the Manifest, false if not.
|
||||
*/
|
||||
fun hasDefinedPermission(context: Context, permission: String): Boolean {
|
||||
var hasPermission = false
|
||||
val requestedPermissions = getManifestPermissions(context)
|
||||
if (!requestedPermissions.isNullOrEmpty()) {
|
||||
val requestedPermissionsList = listOf(*requestedPermissions)
|
||||
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
|
||||
if (requestedPermissionsArrayList.contains(permission)) {
|
||||
hasPermission = true
|
||||
}
|
||||
}
|
||||
return hasPermission
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
|
||||
* @param context the app context
|
||||
* @param permissions a list of permissions
|
||||
* @return true only if all permissions are defined in the AndroidManifest.xml
|
||||
*/
|
||||
fun hasDefinedPermissions(context: Context, permissions: Array<String>): Boolean {
|
||||
for (permission in permissions) {
|
||||
if (!hasDefinedPermission(context, permission)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permissions defined in AndroidManifest.xml
|
||||
*
|
||||
* @return The permissions defined in AndroidManifest.xml
|
||||
*/
|
||||
private fun getManifestPermissions(context: Context): Array<String>? {
|
||||
var requestedPermissions: Array<String>? = null
|
||||
try {
|
||||
val pm = context.packageManager
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
|
||||
}
|
||||
if (packageInfo != null) {
|
||||
requestedPermissions = packageInfo.requestedPermissions
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return requestedPermissions
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
|
||||
*
|
||||
* @param neededPermissions The permissions needed.
|
||||
* @return The permissions not present in AndroidManifest.xml
|
||||
*/
|
||||
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String?>): Array<String?> {
|
||||
val undefinedPermissions = ArrayList<String?>()
|
||||
val requestedPermissions = getManifestPermissions(context)
|
||||
if (!requestedPermissions.isNullOrEmpty()) {
|
||||
val requestedPermissionsList = listOf(*requestedPermissions)
|
||||
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
|
||||
for (permission in neededPermissions) {
|
||||
if (!requestedPermissionsArrayList.contains(permission)) {
|
||||
undefinedPermissions.add(permission)
|
||||
}
|
||||
}
|
||||
var undefinedPermissionArray = arrayOfNulls<String>(undefinedPermissions.size)
|
||||
undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray)
|
||||
return undefinedPermissionArray
|
||||
}
|
||||
return neededPermissions
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package {{package}}
|
||||
|
||||
import android.content.Intent
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
|
||||
object Rust {
|
||||
init {
|
||||
System.loadLibrary("{{library}}")
|
||||
}
|
||||
|
||||
@JvmStatic external fun onActivityCreate(activity: WryActivity)
|
||||
@JvmStatic external fun onActivityDestroy(activity: WryActivity)
|
||||
@JvmStatic external fun onActivitySaveInstanceState()
|
||||
@JvmStatic external fun onActivityLowMemory()
|
||||
@JvmStatic external fun onWindowFocusChanged(activity: WryActivity, focus: Boolean)
|
||||
@JvmStatic external fun onNewIntent(intent: Intent)
|
||||
|
||||
@JvmStatic external fun create()
|
||||
@JvmStatic external fun start()
|
||||
@JvmStatic external fun resume()
|
||||
@JvmStatic external fun pause()
|
||||
@JvmStatic external fun stop()
|
||||
|
||||
@JvmStatic external fun wryCreate()
|
||||
@JvmStatic external fun onWebviewDestroy(activity: WryActivity, webviewId: String)
|
||||
|
||||
@JvmStatic external fun ipc(webviewId: String, url: String, message: String)
|
||||
|
||||
@JvmStatic external fun assetLoaderDomain(webviewId: String): String
|
||||
@JvmStatic external fun withAssetLoader(webviewId: String): Boolean
|
||||
@JvmStatic external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse?
|
||||
@JvmStatic external fun shouldOverride(webviewId: String, url: String): Boolean
|
||||
@JvmStatic external fun onPageLoading(webviewId: String, url: String)
|
||||
@JvmStatic external fun onPageLoaded(webviewId: String, url: String)
|
||||
@JvmStatic external fun onEval(webviewId: String, id: Int, result: String)
|
||||
|
||||
@JvmStatic external fun handleReceivedTitle(webviewId: String, title: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("ObsoleteSdkInt", "RedundantOverride", "QueryPermissionsNeeded", "SimpleDateFormat")
|
||||
|
||||
package {{package}}
|
||||
|
||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.view.View
|
||||
import android.webkit.*
|
||||
import android.widget.EditText
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
|
||||
private interface PermissionListener {
|
||||
fun onPermissionSelect(isGranted: Boolean?)
|
||||
}
|
||||
|
||||
private interface ActivityResultListener {
|
||||
fun onActivityResult(result: ActivityResult?)
|
||||
}
|
||||
|
||||
private val activity: WryActivity
|
||||
private var permissionLauncher: ActivityResultLauncher<Array<String>>
|
||||
private var activityLauncher: ActivityResultLauncher<Intent>
|
||||
private var permissionListener: PermissionListener? = null
|
||||
private var activityListener: ActivityResultListener? = null
|
||||
|
||||
init {
|
||||
activity = appActivity
|
||||
val permissionCallback =
|
||||
ActivityResultCallback { isGranted: Map<String, Boolean> ->
|
||||
if (permissionListener != null) {
|
||||
var granted = true
|
||||
for ((_, value) in isGranted) {
|
||||
if (!value) granted = false
|
||||
}
|
||||
permissionListener!!.onPermissionSelect(granted)
|
||||
}
|
||||
}
|
||||
permissionLauncher =
|
||||
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback)
|
||||
activityLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (activityListener != null) {
|
||||
activityListener!!.onActivityResult(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render web content in `view`.
|
||||
*
|
||||
* Both this method and [.onHideCustomView] are required for
|
||||
* rendering web content in full screen.
|
||||
*
|
||||
* @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView
|
||||
) */
|
||||
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
|
||||
callback.onCustomViewHidden()
|
||||
super.onShowCustomView(view, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render web content in the original Web View again.
|
||||
*
|
||||
* Do not remove this method--@see #onShowCustomView(View, CustomViewCallback).
|
||||
*/
|
||||
override fun onHideCustomView() {
|
||||
super.onHideCustomView()
|
||||
}
|
||||
|
||||
override fun onPermissionRequest(request: PermissionRequest) {
|
||||
val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
val permissionList: MutableList<String> = ArrayList()
|
||||
if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
|
||||
permissionList.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) {
|
||||
permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS)
|
||||
permissionList.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
if (permissionList.isNotEmpty() && isRequestPermissionRequired) {
|
||||
val permissions = permissionList.toTypedArray()
|
||||
permissionListener = object : PermissionListener {
|
||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
||||
if (isGranted == true) {
|
||||
request.grant(request.resources)
|
||||
} else {
|
||||
request.deny()
|
||||
}
|
||||
}
|
||||
}
|
||||
permissionLauncher.launch(permissions)
|
||||
} else {
|
||||
request.grant(request.resources)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser alert modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
|
||||
if (activity.isFinishing) {
|
||||
return true
|
||||
}
|
||||
val builder = AlertDialog.Builder(view.context)
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
"OK"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
result.confirm()
|
||||
}
|
||||
.setOnCancelListener { dialog: DialogInterface ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser confirm modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean {
|
||||
if (activity.isFinishing) {
|
||||
return true
|
||||
}
|
||||
val builder = AlertDialog.Builder(view.context)
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
"OK"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
result.confirm()
|
||||
}
|
||||
.setNegativeButton(
|
||||
"Cancel"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
.setOnCancelListener { dialog: DialogInterface ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser prompt modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param defaultValue
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
override fun onJsPrompt(
|
||||
view: WebView,
|
||||
url: String,
|
||||
message: String,
|
||||
defaultValue: String,
|
||||
result: JsPromptResult
|
||||
): Boolean {
|
||||
if (activity.isFinishing) {
|
||||
return true
|
||||
}
|
||||
val builder = AlertDialog.Builder(view.context)
|
||||
val input = EditText(view.context)
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setView(input)
|
||||
.setPositiveButton(
|
||||
"OK"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
val inputText1 = input.text.toString().trim { it <= ' ' }
|
||||
result.confirm(inputText1)
|
||||
}
|
||||
.setNegativeButton(
|
||||
"Cancel"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
.setOnCancelListener { dialog: DialogInterface ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the browser geolocation permission prompt
|
||||
* @param origin
|
||||
* @param callback
|
||||
*/
|
||||
override fun onGeolocationPermissionsShowPrompt(
|
||||
origin: String,
|
||||
callback: GeolocationPermissions.Callback
|
||||
) {
|
||||
super.onGeolocationPermissionsShowPrompt(origin, callback)
|
||||
Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin")
|
||||
val geoPermissions =
|
||||
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
if (!PermissionHelper.hasPermissions(activity, geoPermissions)) {
|
||||
permissionListener = object : PermissionListener {
|
||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
||||
if (isGranted == true) {
|
||||
callback.invoke(origin, true, false)
|
||||
} else {
|
||||
val coarsePermission =
|
||||
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
PermissionHelper.hasPermissions(activity, coarsePermission)
|
||||
) {
|
||||
callback.invoke(origin, true, false)
|
||||
} else {
|
||||
callback.invoke(origin, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
permissionLauncher.launch(geoPermissions)
|
||||
} else {
|
||||
// permission is already granted
|
||||
callback.invoke(origin, true, false)
|
||||
Logger.debug("onGeolocationPermissionsShowPrompt: has required permission")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShowFileChooser(
|
||||
webView: WebView,
|
||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
||||
fileChooserParams: FileChooserParams
|
||||
): Boolean {
|
||||
val acceptTypes = listOf(*fileChooserParams.acceptTypes)
|
||||
val captureEnabled = fileChooserParams.isCaptureEnabled
|
||||
val capturePhoto = captureEnabled && acceptTypes.contains("image/*")
|
||||
val captureVideo = captureEnabled && acceptTypes.contains("video/*")
|
||||
if (capturePhoto || captureVideo) {
|
||||
if (isMediaCaptureSupported) {
|
||||
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
|
||||
} else {
|
||||
permissionListener = object : PermissionListener {
|
||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
||||
if (isGranted == true) {
|
||||
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
|
||||
} else {
|
||||
Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted")
|
||||
filePathCallback.onReceiveValue(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
val camPermission = arrayOf(Manifest.permission.CAMERA)
|
||||
permissionLauncher.launch(camPermission)
|
||||
}
|
||||
} else {
|
||||
showFilePicker(filePathCallback, fileChooserParams)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val isMediaCaptureSupported: Boolean
|
||||
get() {
|
||||
val permissions = arrayOf(Manifest.permission.CAMERA)
|
||||
return PermissionHelper.hasPermissions(activity, permissions) ||
|
||||
!PermissionHelper.hasDefinedPermission(activity, Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
private fun showMediaCaptureOrFilePicker(
|
||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
||||
fileChooserParams: FileChooserParams,
|
||||
isVideo: Boolean
|
||||
) {
|
||||
val isVideoCaptureSupported = true
|
||||
val shown = if (isVideo && isVideoCaptureSupported) {
|
||||
showVideoCapturePicker(filePathCallback)
|
||||
} else {
|
||||
showImageCapturePicker(filePathCallback)
|
||||
}
|
||||
if (!shown) {
|
||||
Logger.warn(
|
||||
Logger.tags("FileChooser"),
|
||||
"Media capture intent could not be launched. Falling back to default file picker."
|
||||
)
|
||||
showFilePicker(filePathCallback, fileChooserParams)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showImageCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
|
||||
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
if (takePictureIntent.resolveActivity(activity.packageManager) == null) {
|
||||
return false
|
||||
}
|
||||
val imageFileUri: Uri = try {
|
||||
createImageFileUri()
|
||||
} catch (ex: Exception) {
|
||||
Logger.error("Unable to create temporary media capture file: " + ex.message)
|
||||
return false
|
||||
}
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri)
|
||||
activityListener = object : ActivityResultListener {
|
||||
override fun onActivityResult(result: ActivityResult?) {
|
||||
var res: Array<Uri?>? = null
|
||||
if (result?.resultCode == Activity.RESULT_OK) {
|
||||
res = arrayOf(imageFileUri)
|
||||
}
|
||||
filePathCallback.onReceiveValue(res)
|
||||
}
|
||||
}
|
||||
activityLauncher.launch(takePictureIntent)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showVideoCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
|
||||
val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
|
||||
if (takeVideoIntent.resolveActivity(activity.packageManager) == null) {
|
||||
return false
|
||||
}
|
||||
activityListener = object : ActivityResultListener {
|
||||
override fun onActivityResult(result: ActivityResult?) {
|
||||
var res: Array<Uri?>? = null
|
||||
if (result?.resultCode == Activity.RESULT_OK) {
|
||||
res = arrayOf(result.data!!.data)
|
||||
}
|
||||
filePathCallback.onReceiveValue(res)
|
||||
}
|
||||
}
|
||||
activityLauncher.launch(takeVideoIntent)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showFilePicker(
|
||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
||||
fileChooserParams: FileChooserParams
|
||||
) {
|
||||
val intent = fileChooserParams.createIntent()
|
||||
if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
}
|
||||
if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) {
|
||||
val validTypes = getValidTypes(fileChooserParams.acceptTypes)
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes)
|
||||
if (intent.type!!.startsWith(".")) {
|
||||
intent.type = validTypes[0]
|
||||
}
|
||||
}
|
||||
try {
|
||||
activityListener = object : ActivityResultListener {
|
||||
override fun onActivityResult(result: ActivityResult?) {
|
||||
val res: Array<Uri?>?
|
||||
val resultIntent = result?.data
|
||||
if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) {
|
||||
val numFiles = resultIntent.clipData!!.itemCount
|
||||
res = arrayOfNulls(numFiles)
|
||||
for (i in 0 until numFiles) {
|
||||
res[i] = resultIntent.clipData!!.getItemAt(i).uri
|
||||
}
|
||||
} else {
|
||||
res = FileChooserParams.parseResult(
|
||||
result?.resultCode ?: 0,
|
||||
resultIntent
|
||||
)
|
||||
}
|
||||
filePathCallback.onReceiveValue(res)
|
||||
}
|
||||
}
|
||||
activityLauncher.launch(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
filePathCallback.onReceiveValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValidTypes(currentTypes: Array<String>): Array<String> {
|
||||
val validTypes: MutableList<String> = ArrayList()
|
||||
val mtm = MimeTypeMap.getSingleton()
|
||||
for (mime in currentTypes) {
|
||||
if (mime.startsWith(".")) {
|
||||
val extension = mime.substring(1)
|
||||
val extensionMime = mtm.getMimeTypeFromExtension(extension)
|
||||
if (extensionMime != null && !validTypes.contains(extensionMime)) {
|
||||
validTypes.add(extensionMime)
|
||||
}
|
||||
} else if (!validTypes.contains(mime)) {
|
||||
validTypes.add(mime)
|
||||
}
|
||||
}
|
||||
val validObj: Array<Any> = validTypes.toTypedArray()
|
||||
return Arrays.copyOf(
|
||||
validObj, validObj.size,
|
||||
Array<String>::class.java
|
||||
)
|
||||
}
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
||||
val tag: String = Logger.tags("Console")
|
||||
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
|
||||
val msg = String.format(
|
||||
"File: %s - Line %d - Msg: %s",
|
||||
consoleMessage.sourceId(),
|
||||
consoleMessage.lineNumber(),
|
||||
consoleMessage.message()
|
||||
)
|
||||
val level = consoleMessage.messageLevel().name
|
||||
if ("ERROR".equals(level, ignoreCase = true)) {
|
||||
Logger.error(tag, msg, null)
|
||||
} else if ("WARNING".equals(level, ignoreCase = true)) {
|
||||
Logger.warn(tag, msg)
|
||||
} else if ("TIP".equals(level, ignoreCase = true)) {
|
||||
Logger.debug(tag, msg)
|
||||
} else {
|
||||
Logger.info(tag, msg)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isValidMsg(msg: String): Boolean {
|
||||
return !(msg.contains("%cresult %c") ||
|
||||
msg.contains("%cnative %c") ||
|
||||
msg.equals("[object Object]", ignoreCase = true) ||
|
||||
msg.equals("console.groupEnd", ignoreCase = true))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun createImageFileUri(): Uri {
|
||||
val photoFile = createImageFile(activity)
|
||||
return FileProvider.getUriForFile(
|
||||
activity,
|
||||
activity.packageName.toString() + ".fileprovider",
|
||||
photoFile
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun createImageFile(activity: Activity): File {
|
||||
// Create an image file name
|
||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
|
||||
val imageFileName = "JPEG_" + timeStamp + "_"
|
||||
val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||
return File.createTempFile(imageFileName, ".jpg", storageDir)
|
||||
}
|
||||
|
||||
override fun onReceivedTitle(
|
||||
view: WebView,
|
||||
title: String
|
||||
) {
|
||||
Rust.handleReceivedTitle((view as RustWebView).id, title)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("unused", "SetJavaScriptEnabled")
|
||||
|
||||
package {{package}}
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.webkit.*
|
||||
import android.content.Context
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import kotlin.collections.Map
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
class RustWebView(context: Context, val initScripts: Array<String>, val id: String): WebView(context) {
|
||||
val isDocumentStartScriptEnabled: Boolean
|
||||
|
||||
init {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.setGeolocationEnabled(true)
|
||||
settings.databaseEnabled = true
|
||||
settings.mediaPlaybackRequiresUserGesture = false
|
||||
settings.javaScriptCanOpenWindowsAutomatically = true
|
||||
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
||||
isDocumentStartScriptEnabled = true
|
||||
for (script in initScripts) {
|
||||
WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*"));
|
||||
}
|
||||
} else {
|
||||
isDocumentStartScriptEnabled = false
|
||||
}
|
||||
|
||||
{{class-init}}
|
||||
}
|
||||
|
||||
fun loadUrlMainThread(url: String) {
|
||||
post {
|
||||
loadUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadUrlMainThread(url: String, additionalHttpHeaders: Map<String, String>) {
|
||||
post {
|
||||
loadUrl(url, additionalHttpHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadUrl(url: String) {
|
||||
if (!Rust.shouldOverride(id, url)) {
|
||||
super.loadUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadUrl(url: String, additionalHttpHeaders: Map<String, String>) {
|
||||
if (!Rust.shouldOverride(id, url)) {
|
||||
super.loadUrl(url, additionalHttpHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
fun loadHTMLMainThread(html: String) {
|
||||
post {
|
||||
super.loadData(html, "text/html", null)
|
||||
}
|
||||
}
|
||||
|
||||
fun evalScript(id: Int, script: String) {
|
||||
post {
|
||||
super.evaluateJavascript(script) { result ->
|
||||
Rust.onEval(this.id, id, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAllBrowsingData() {
|
||||
try {
|
||||
super.getContext().deleteDatabase("webviewCache.db")
|
||||
super.getContext().deleteDatabase("webview.db")
|
||||
super.clearCache(true)
|
||||
super.clearHistory()
|
||||
super.clearFormData()
|
||||
} catch (ex: Exception) {
|
||||
Logger.error("Unable to create temporary media capture file: " + ex.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCookies(url: String): String {
|
||||
val cookieManager = CookieManager.getInstance()
|
||||
return cookieManager.getCookie(url)
|
||||
}
|
||||
|
||||
{{class-extension}}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package {{package}}
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.*
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
|
||||
class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient() {
|
||||
private val interceptedState = mutableMapOf<String, Boolean>()
|
||||
var currentUrl: String = "about:blank"
|
||||
private var lastInterceptedUrl: Uri? = null
|
||||
private var pendingUrlRedirect: String? = null
|
||||
|
||||
private val assetLoader = WebViewAssetLoader.Builder()
|
||||
.setDomain(Rust.assetLoaderDomain(webView.id))
|
||||
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context))
|
||||
.build()
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
pendingUrlRedirect?.let {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
view.loadUrl(it)
|
||||
}
|
||||
pendingUrlRedirect = null
|
||||
return null
|
||||
}
|
||||
|
||||
lastInterceptedUrl = request.url
|
||||
return if (Rust.withAssetLoader((view as RustWebView).id)) {
|
||||
assetLoader.shouldInterceptRequest(request.url)
|
||||
} else {
|
||||
val response = Rust.handleRequest(view.id, request, view.isDocumentStartScriptEnabled)
|
||||
if (response != null) {
|
||||
if (response.responseHeaders != null) {
|
||||
response.responseHeaders["Cache-Control"] = "no-store"
|
||||
} else {
|
||||
response.responseHeaders = mapOf("Cache-Control" to "no-store")
|
||||
}
|
||||
}
|
||||
interceptedState[request.url.toString()] = response != null
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): Boolean {
|
||||
return Rust.shouldOverride((view as RustWebView).id, request.url.toString())
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
currentUrl = url
|
||||
if (interceptedState[url] == false) {
|
||||
val webView = view as RustWebView
|
||||
for (script in webView.initScripts) {
|
||||
view.evaluateJavascript(script, null)
|
||||
}
|
||||
}
|
||||
return Rust.onPageLoading((view as RustWebView).id, url)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
Rust.onPageLoaded((view as RustWebView).id, url)
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError
|
||||
) {
|
||||
// we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol
|
||||
// e.g. oauth flow, because shouldInterceptRequest is not called on redirects
|
||||
// so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in
|
||||
if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) {
|
||||
// prevent the default error page from showing
|
||||
view.stopLoading()
|
||||
// without this initial loadUrl the app is stuck
|
||||
view.loadUrl(request.url.toString())
|
||||
// ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later
|
||||
pendingUrlRedirect = request.url.toString()
|
||||
} else {
|
||||
super.onReceivedError(view, request, error)
|
||||
}
|
||||
}
|
||||
|
||||
{{class-extension}}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package {{package}}
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
|
||||
private val ACTIVITY_ID_KEY = "__wryActivityId"
|
||||
|
||||
object WryLifecycleObserver : DefaultLifecycleObserver {
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
super.onCreate(owner)
|
||||
Rust.create()
|
||||
Rust.wryCreate()
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
super.onStart(owner)
|
||||
Rust.start()
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
Rust.resume()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
super.onPause(owner)
|
||||
Rust.pause()
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
super.onStop(owner)
|
||||
Rust.stop()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class WryActivity : AppCompatActivity() {
|
||||
private lateinit var mWebView: RustWebView
|
||||
var id: Int = 0
|
||||
open val handleBackNavigation: Boolean = true
|
||||
|
||||
open fun onWebViewCreate(webView: WebView) { }
|
||||
|
||||
fun setWebView(webView: RustWebView) {
|
||||
mWebView = webView
|
||||
|
||||
if (handleBackNavigation) {
|
||||
val callback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (this@WryActivity::mWebView.isInitialized) {
|
||||
if (this@WryActivity.mWebView.canGoBack()) {
|
||||
this@WryActivity.mWebView.goBack()
|
||||
} else {
|
||||
this.isEnabled = false
|
||||
this@WryActivity.onBackPressed()
|
||||
this.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, callback)
|
||||
}
|
||||
|
||||
onWebViewCreate(webView)
|
||||
}
|
||||
|
||||
val version: String
|
||||
@SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt")
|
||||
get() {
|
||||
// Check getCurrentWebViewPackage() directly if above Android 8
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
return WebView.getCurrentWebViewPackage()?.versionName ?: ""
|
||||
}
|
||||
|
||||
// Otherwise manually check WebView versions
|
||||
var webViewPackage = "com.google.android.webview"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
webViewPackage = "com.android.chrome"
|
||||
}
|
||||
try {
|
||||
@Suppress("DEPRECATION")
|
||||
val info = packageManager.getPackageInfo(webViewPackage, 0)
|
||||
return info.versionName.toString()
|
||||
} catch (ex: Exception) {
|
||||
Logger.warn("Unable to get package info for '$webViewPackage'$ex")
|
||||
}
|
||||
|
||||
try {
|
||||
@Suppress("DEPRECATION")
|
||||
val info = packageManager.getPackageInfo("com.android.webview", 0)
|
||||
return info.versionName.toString()
|
||||
} catch (ex: Exception) {
|
||||
Logger.warn("Unable to get package info for 'com.android.webview'$ex")
|
||||
}
|
||||
|
||||
// Could not detect any webview, return empty string
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver)
|
||||
Rust.onActivityCreate(this)
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
Rust.onWindowFocusChanged(this, hasFocus)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt(ACTIVITY_ID_KEY, id)
|
||||
Rust.onActivitySaveInstanceState()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (::mWebView.isInitialized) {
|
||||
mWebView.onPause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (::mWebView.isInitialized) {
|
||||
mWebView.onResume()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Rust.onActivityDestroy(this)
|
||||
Rust.onWebviewDestroy(this, if (::mWebView.isInitialized) { mWebView.id } else { "" })
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
Rust.onActivityLowMemory()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Rust.onNewIntent(intent)
|
||||
}
|
||||
|
||||
fun getAppClass(name: String): Class<*> {
|
||||
return Class.forName(name)
|
||||
}
|
||||
|
||||
fun startActivity(cls: Class<*>): Int {
|
||||
val intent = Intent(this, cls)
|
||||
val id = kotlin.random.Random.nextInt()
|
||||
intent.putExtra(ACTIVITY_ID_KEY, id)
|
||||
startActivity(intent)
|
||||
return id
|
||||
}
|
||||
|
||||
{{class-extension}}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
-keep class {{package-unescaped}}.* {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keep class {{package-unescaped}}.WryActivity {
|
||||
public <init>(...);
|
||||
|
||||
void setWebView({{package-unescaped}}.RustWebView);
|
||||
java.lang.Class getAppClass(...);
|
||||
int getId();
|
||||
java.lang.String getVersion();
|
||||
int startActivity(...);
|
||||
}
|
||||
|
||||
-keep class {{package-unescaped}}.Ipc {
|
||||
public <init>(...);
|
||||
|
||||
@android.webkit.JavascriptInterface public <methods>;
|
||||
}
|
||||
|
||||
-keep class {{package-unescaped}}.RustWebView {
|
||||
public <init>(...);
|
||||
|
||||
void loadUrlMainThread(...);
|
||||
void loadHTMLMainThread(...);
|
||||
void evalScript(...);
|
||||
}
|
||||
|
||||
-keep class {{package-unescaped}}.RustWebChromeClient,{{package-unescaped}}.RustWebViewClient {
|
||||
public <init>(...);
|
||||
}
|
||||
|
|
@ -0,0 +1,607 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{Error, InitializationScript, RGBA};
|
||||
use crossbeam_channel::*;
|
||||
use jni::{
|
||||
errors::Result as JniResult,
|
||||
objects::{GlobalRef, JMap, JObject, JString},
|
||||
JNIEnv, JavaVM,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ffi::c_void,
|
||||
os::unix::prelude::*,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use super::{find_class, EvalCallback, WebviewId, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE};
|
||||
|
||||
pub type ActivityId = i32;
|
||||
|
||||
static CHANNEL: Lazy<(
|
||||
Sender<(ActivityId, WebViewMessage)>,
|
||||
Receiver<(ActivityId, WebViewMessage)>,
|
||||
)> = Lazy::new(|| bounded(8));
|
||||
pub static MAIN_PIPE: Lazy<[OwnedFd; 2]> = Lazy::new(|| {
|
||||
let mut pipe: [RawFd; 2] = Default::default();
|
||||
unsafe { libc::pipe(pipe.as_mut_ptr()) };
|
||||
unsafe { pipe.map(|fd| OwnedFd::from_raw_fd(fd)) }
|
||||
});
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ActivityProxy {
|
||||
pub activity: GlobalRef,
|
||||
pub window_manager: GlobalRef,
|
||||
pub webview: Option<GlobalRef>,
|
||||
pub webchrome_client: GlobalRef,
|
||||
pub java_vm: *mut c_void,
|
||||
}
|
||||
|
||||
unsafe impl Send for ActivityProxy {}
|
||||
|
||||
impl ActivityProxy {
|
||||
pub fn new(
|
||||
vm: JavaVM,
|
||||
activity: GlobalRef,
|
||||
window_manager: GlobalRef,
|
||||
webchrome_client: GlobalRef,
|
||||
) -> Self {
|
||||
Self {
|
||||
activity,
|
||||
window_manager,
|
||||
webview: None,
|
||||
webchrome_client,
|
||||
java_vm: vm.get_java_vm_pointer() as *mut _,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static ACTIVITY_PROXY: once_cell::sync::Lazy<Mutex<BTreeMap<ActivityId, ActivityProxy>>> =
|
||||
Lazy::new(|| Mutex::new(BTreeMap::new()));
|
||||
|
||||
pub fn activity_proxy(id: ActivityId) -> Option<ActivityProxy> {
|
||||
ACTIVITY_PROXY.lock().unwrap().get(&id).cloned()
|
||||
}
|
||||
|
||||
fn remove_activity_proxy(id: ActivityId) {
|
||||
ACTIVITY_PROXY.lock().unwrap().remove(&id);
|
||||
}
|
||||
|
||||
pub fn register_activity_proxy(
|
||||
vm: JavaVM,
|
||||
id: ActivityId,
|
||||
activity: GlobalRef,
|
||||
window_manager: GlobalRef,
|
||||
webchrome_client: GlobalRef,
|
||||
) {
|
||||
let mut activity_proxy = ACTIVITY_PROXY.lock().unwrap();
|
||||
if let Some(proxy) = activity_proxy.get_mut(&id) {
|
||||
proxy.activity = activity;
|
||||
proxy.window_manager = window_manager;
|
||||
proxy.webchrome_client = webchrome_client;
|
||||
proxy.java_vm = vm.get_java_vm_pointer() as *mut _;
|
||||
} else {
|
||||
let proxy = ActivityProxy::new(vm, activity, window_manager, webchrome_client);
|
||||
activity_proxy.insert(id, proxy.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activity_id_for_window_manager(window_manager: JObject) -> Option<ActivityId> {
|
||||
for (activity_id, proxy) in ACTIVITY_PROXY.lock().unwrap().iter() {
|
||||
let vm = unsafe { JavaVM::from_raw(proxy.java_vm.cast()) }.unwrap();
|
||||
let mut env = vm.attach_current_thread_as_daemon().unwrap();
|
||||
let equals = env
|
||||
.call_method(
|
||||
proxy.window_manager.as_obj(),
|
||||
"equals",
|
||||
"(Ljava/lang/Object;)Z",
|
||||
&[(&window_manager).into()],
|
||||
)
|
||||
.and_then(|v| v.z())
|
||||
.unwrap_or_default();
|
||||
if equals {
|
||||
return Some(*activity_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn first_activity_id() -> Option<ActivityId> {
|
||||
ACTIVITY_PROXY.lock().unwrap().keys().next().cloned()
|
||||
}
|
||||
|
||||
pub fn get_webview(activity_id: ActivityId) -> Option<GlobalRef> {
|
||||
ACTIVITY_PROXY
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&activity_id)
|
||||
.unwrap()
|
||||
.webview
|
||||
.as_ref()
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub struct MainPipe<'a> {
|
||||
pub env: JNIEnv<'a>,
|
||||
}
|
||||
|
||||
impl<'a> MainPipe<'a> {
|
||||
pub(crate) fn send(activity_id: ActivityId, message: WebViewMessage) {
|
||||
let size = std::mem::size_of::<bool>();
|
||||
if CHANNEL.0.send((activity_id, message)).is_ok() {
|
||||
unsafe {
|
||||
libc::write(
|
||||
MAIN_PIPE[1].as_raw_fd(),
|
||||
&true as *const _ as *const _,
|
||||
size,
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv(&mut self) -> JniResult<()> {
|
||||
if let Ok((activity_id, message)) = CHANNEL.1.recv() {
|
||||
match message {
|
||||
WebViewMessage::CreateWebView(attrs) => {
|
||||
let Some((activity, web_chrome_client)) =
|
||||
activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webchrome_client.clone()))
|
||||
else {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("no activity found for activity id: {}", activity_id);
|
||||
return Ok(());
|
||||
};
|
||||
let CreateWebViewAttributes {
|
||||
url,
|
||||
html,
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
devtools,
|
||||
transparent,
|
||||
background_color,
|
||||
headers,
|
||||
on_webview_created,
|
||||
autoplay,
|
||||
user_agent,
|
||||
initialization_scripts,
|
||||
id,
|
||||
javascript_disabled,
|
||||
..
|
||||
} = attrs;
|
||||
|
||||
let string_class = self.env.find_class("java/lang/String")?;
|
||||
let initialization_scripts_array = self.env.new_object_array(
|
||||
initialization_scripts.len() as i32,
|
||||
string_class,
|
||||
self.env.new_string("")?,
|
||||
)?;
|
||||
for (i, init_script) in initialization_scripts.into_iter().enumerate() {
|
||||
self.env.set_object_array_element(
|
||||
&initialization_scripts_array,
|
||||
i as i32,
|
||||
self.env.new_string(init_script.script)?,
|
||||
)?;
|
||||
}
|
||||
let id = self.env.new_string(id)?;
|
||||
// Create webview
|
||||
let rust_webview_class = find_class(
|
||||
&mut self.env,
|
||||
&activity,
|
||||
format!("{}/RustWebView", PACKAGE.get().unwrap()),
|
||||
)?;
|
||||
let webview = self.env.new_object(
|
||||
&rust_webview_class,
|
||||
"(Landroid/content/Context;[Ljava/lang/String;Ljava/lang/String;)V",
|
||||
&[
|
||||
(&activity).into(),
|
||||
(&initialization_scripts_array).into(),
|
||||
(&id).into(),
|
||||
],
|
||||
)?;
|
||||
// get settings
|
||||
let web_settings = self
|
||||
.env
|
||||
.call_method(
|
||||
&webview,
|
||||
"getSettings",
|
||||
"()Landroid/webkit/WebSettings;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
// set media autoplay
|
||||
self.env.call_method(
|
||||
&web_settings,
|
||||
"setMediaPlaybackRequiresUserGesture",
|
||||
"(Z)V",
|
||||
&[(!autoplay).into()],
|
||||
)?;
|
||||
// set user-agent
|
||||
if let Some(user_agent) = user_agent {
|
||||
let user_agent = self.env.new_string(user_agent)?;
|
||||
self.env.call_method(
|
||||
&web_settings,
|
||||
"setUserAgentString",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[(&user_agent).into()],
|
||||
)?;
|
||||
}
|
||||
|
||||
// disable javascript
|
||||
if javascript_disabled {
|
||||
self.env.call_method(
|
||||
&web_settings,
|
||||
"setJavaScriptEnabled",
|
||||
"(Z)V",
|
||||
&[false.into()],
|
||||
)?;
|
||||
}
|
||||
|
||||
let webview_class_name = format!("{}/RustWebView", PACKAGE.get().unwrap());
|
||||
self.env.call_method(
|
||||
&activity,
|
||||
"setWebView",
|
||||
format!("(L{webview_class_name};)V"),
|
||||
&[(&webview).into()],
|
||||
)?;
|
||||
// Navigation
|
||||
if let Some(u) = url {
|
||||
if let Ok(url) = self.env.new_string(u) {
|
||||
load_url(&mut self.env, &webview, &url, headers, true)?;
|
||||
}
|
||||
} else if let Some(h) = html {
|
||||
if let Ok(html) = self.env.new_string(h) {
|
||||
load_html(&mut self.env, &webview, &html)?;
|
||||
}
|
||||
}
|
||||
// Enable devtools
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
self.env.call_static_method(
|
||||
&rust_webview_class,
|
||||
"setWebContentsDebuggingEnabled",
|
||||
"(Z)V",
|
||||
&[devtools.into()],
|
||||
)?;
|
||||
if transparent {
|
||||
set_background_color(&mut self.env, &webview, (0, 0, 0, 0))?;
|
||||
} else if let Some(color) = background_color {
|
||||
set_background_color(&mut self.env, &webview, color)?;
|
||||
}
|
||||
// Create and set webview client
|
||||
let client_class_name = format!("{}/RustWebViewClient", PACKAGE.get().unwrap());
|
||||
let rust_webview_client_class =
|
||||
find_class(&mut self.env, &activity, client_class_name.clone())?;
|
||||
let webview_client = self.env.new_object(
|
||||
&rust_webview_client_class,
|
||||
format!("(L{webview_class_name};Landroid/content/Context;)V"),
|
||||
&[(&webview).into(), (&activity).into()],
|
||||
)?;
|
||||
self.env.call_method(
|
||||
&webview,
|
||||
"setWebViewClient",
|
||||
"(Landroid/webkit/WebViewClient;)V",
|
||||
&[(&webview_client).into()],
|
||||
)?;
|
||||
// set webchrome client
|
||||
self.env.call_method(
|
||||
&webview,
|
||||
"setWebChromeClient",
|
||||
"(Landroid/webkit/WebChromeClient;)V",
|
||||
&[web_chrome_client.as_obj().into()],
|
||||
)?;
|
||||
|
||||
// Add javascript interface (IPC)
|
||||
let ipc_class = find_class(
|
||||
&mut self.env,
|
||||
&activity,
|
||||
format!("{}/Ipc", PACKAGE.get().unwrap()),
|
||||
)?;
|
||||
let ipc = self.env.new_object(
|
||||
ipc_class,
|
||||
format!("(L{webview_class_name};L{client_class_name};)V"),
|
||||
&[(&webview).into(), (&webview_client).into()],
|
||||
)?;
|
||||
let ipc_str = self.env.new_string("ipc")?;
|
||||
self.env.call_method(
|
||||
&webview,
|
||||
"addJavascriptInterface",
|
||||
"(Ljava/lang/Object;Ljava/lang/String;)V",
|
||||
&[(&ipc).into(), (&ipc_str).into()],
|
||||
)?;
|
||||
|
||||
// Set content view
|
||||
self.env.call_method(
|
||||
&activity,
|
||||
"setContentView",
|
||||
"(Landroid/view/View;)V",
|
||||
&[(&webview).into()],
|
||||
)?;
|
||||
|
||||
if let Some(on_webview_created) = on_webview_created {
|
||||
if let Err(_e) = on_webview_created(super::Context {
|
||||
env: &mut self.env,
|
||||
activity: &activity,
|
||||
webview: &webview,
|
||||
}) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("failed to run webview created hook: {_e}");
|
||||
}
|
||||
}
|
||||
|
||||
let webview = self.env.new_global_ref(webview)?;
|
||||
|
||||
ACTIVITY_PROXY
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_mut(&activity_id)
|
||||
.unwrap()
|
||||
.webview
|
||||
.replace(webview);
|
||||
}
|
||||
WebViewMessage::Eval(script, callback) => {
|
||||
if let Some(webview) = get_webview(activity_id) {
|
||||
let id = EVAL_ID_GENERATOR.next() as i32;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let span = std::sync::Mutex::new(Some(SendEnteredSpan(
|
||||
tracing::debug_span!("wry::eval").entered(),
|
||||
)));
|
||||
|
||||
EVAL_CALLBACKS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(
|
||||
id,
|
||||
Box::new(move |result| {
|
||||
#[cfg(feature = "tracing")]
|
||||
span.lock().unwrap().take();
|
||||
|
||||
if let Some(callback) = &callback {
|
||||
callback(result);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let s = self.env.new_string(script)?;
|
||||
self.env.call_method(
|
||||
webview.as_obj(),
|
||||
"evalScript",
|
||||
"(ILjava/lang/String;)V",
|
||||
&[id.into(), (&s).into()],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
WebViewMessage::SetBackgroundColor(background_color) => {
|
||||
if let Some(webview) = get_webview(activity_id) {
|
||||
set_background_color(&mut self.env, webview.as_obj(), background_color)?;
|
||||
}
|
||||
}
|
||||
WebViewMessage::GetWebViewVersion(tx) => {
|
||||
if let Some(activity) = activity_proxy(activity_id).map(|p| p.activity.clone()) {
|
||||
match self
|
||||
.env
|
||||
.call_method(activity, "getVersion", "()Ljava/lang/String;", &[])
|
||||
.and_then(|v| v.l())
|
||||
.and_then(|s| {
|
||||
let s = JString::from(s);
|
||||
self
|
||||
.env
|
||||
.get_string(&s)
|
||||
.map(|v| v.to_string_lossy().to_string())
|
||||
}) {
|
||||
Ok(version) => {
|
||||
tx.send(Ok(version)).unwrap();
|
||||
}
|
||||
Err(e) => tx.send(Err(e.into())).unwrap(),
|
||||
}
|
||||
} else {
|
||||
tx.send(Err(Error::ActivityNotFound)).unwrap();
|
||||
}
|
||||
}
|
||||
WebViewMessage::GetUrl(tx) => {
|
||||
if let Some(webview) = get_webview(activity_id) {
|
||||
let url = self
|
||||
.env
|
||||
.call_method(webview.as_obj(), "getUrl", "()Ljava/lang/String;", &[])
|
||||
.and_then(|v| v.l())
|
||||
.and_then(|s| {
|
||||
let s = JString::from(s);
|
||||
self
|
||||
.env
|
||||
.get_string(&s)
|
||||
.map(|v| v.to_string_lossy().to_string())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
tx.send(url).unwrap()
|
||||
}
|
||||
}
|
||||
WebViewMessage::Jni(f) => {
|
||||
match activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webview.clone())) {
|
||||
Some((activity, Some(webview))) => {
|
||||
f(&mut self.env, &activity, webview.as_obj());
|
||||
}
|
||||
Some((activity, None)) => {
|
||||
f(&mut self.env, &activity, &JObject::null());
|
||||
}
|
||||
_ => {
|
||||
f(&mut self.env, &JObject::null(), &JObject::null());
|
||||
}
|
||||
}
|
||||
}
|
||||
WebViewMessage::LoadUrl(url, headers) => {
|
||||
if let Some(webview) = get_webview(activity_id) {
|
||||
let url = self.env.new_string(url)?;
|
||||
load_url(&mut self.env, webview.as_obj(), &url, headers, false)?;
|
||||
}
|
||||
}
|
||||
WebViewMessage::ClearAllBrowsingData => {
|
||||
if let Some(webview) = get_webview(activity_id) {
|
||||
self
|
||||
.env
|
||||
.call_method(webview, "clearAllBrowsingData", "()V", &[])?;
|
||||
}
|
||||
}
|
||||
WebViewMessage::LoadHtml(html) => {
|
||||
if let Some(webview) = get_webview(activity_id) {
|
||||
let html = self.env.new_string(html)?;
|
||||
load_html(&mut self.env, webview.as_obj(), &html)?;
|
||||
}
|
||||
}
|
||||
WebViewMessage::Reload => {
|
||||
if let Some(webview) = get_webview(activity_id) {
|
||||
reload(&mut self.env, webview.as_obj())?;
|
||||
}
|
||||
}
|
||||
WebViewMessage::GetCookies(tx, url) => {
|
||||
if let Some(webview) = get_webview(activity_id) {
|
||||
let url = self.env.new_string(url)?;
|
||||
let cookies = self
|
||||
.env
|
||||
.call_method(
|
||||
webview,
|
||||
"getCookies",
|
||||
"(Ljava/lang/String;)Ljava/lang/String;",
|
||||
&[(&url).into()],
|
||||
)
|
||||
.and_then(|v| v.l())
|
||||
.and_then(|s| {
|
||||
let s = JString::from(s);
|
||||
self
|
||||
.env
|
||||
.get_string(&s)
|
||||
.map(|v| v.to_string_lossy().to_string())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
tx.send(
|
||||
cookies
|
||||
.split("; ")
|
||||
.flat_map(|c| cookie::Cookie::parse(c.to_string()))
|
||||
.collect(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
WebViewMessage::OnDestroy {
|
||||
activity_id,
|
||||
webview_id,
|
||||
is_changing_configurations,
|
||||
} => {
|
||||
// keep our webview references (callbacks etc) alive if the activity is going to be recreated due to configuration changes
|
||||
// e.g. rotation, multi-window mode change, etc
|
||||
if !is_changing_configurations {
|
||||
super::destroy_webview(activity_id, &webview_id);
|
||||
remove_activity_proxy(activity_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn load_url<'a>(
|
||||
env: &mut JNIEnv<'a>,
|
||||
webview: &JObject<'a>,
|
||||
url: &JString<'a>,
|
||||
headers: Option<http::HeaderMap>,
|
||||
main_thread: bool,
|
||||
) -> JniResult<()> {
|
||||
let function = if main_thread {
|
||||
"loadUrlMainThread"
|
||||
} else {
|
||||
"loadUrl"
|
||||
};
|
||||
if let Some(headers) = headers {
|
||||
let obj = env.new_object("java/util/HashMap", "()V", &[])?;
|
||||
let headers_map = {
|
||||
let headers_map = JMap::from_env(env, &obj)?;
|
||||
for (name, value) in headers.iter() {
|
||||
let key = env.new_string(name)?;
|
||||
let value = env.new_string(value.to_str().unwrap_or_default())?;
|
||||
headers_map.put(env, &key, &value)?;
|
||||
}
|
||||
headers_map
|
||||
};
|
||||
env.call_method(
|
||||
webview,
|
||||
function,
|
||||
"(Ljava/lang/String;Ljava/util/Map;)V",
|
||||
&[url.into(), (&headers_map).into()],
|
||||
)?;
|
||||
} else {
|
||||
env.call_method(webview, function, "(Ljava/lang/String;)V", &[url.into()])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_html<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>, html: &JString<'a>) -> JniResult<()> {
|
||||
env.call_method(
|
||||
webview,
|
||||
"loadHTMLMainThread",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[html.into()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reload<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>) -> JniResult<()> {
|
||||
env.call_method(webview, "reload", "()V", &[])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_background_color<'a>(
|
||||
env: &mut JNIEnv<'a>,
|
||||
webview: &JObject<'a>,
|
||||
(r, g, b, a): RGBA,
|
||||
) -> JniResult<()> {
|
||||
let color = (a as i32) << 24 | (r as i32) << 16 | (g as i32) << 8 | (b as i32);
|
||||
env.call_method(webview, "setBackgroundColor", "(I)V", &[color.into()])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) enum WebViewMessage {
|
||||
CreateWebView(CreateWebViewAttributes),
|
||||
Eval(String, Option<EvalCallback>),
|
||||
SetBackgroundColor(RGBA),
|
||||
GetWebViewVersion(Sender<Result<String, Error>>),
|
||||
GetUrl(Sender<String>),
|
||||
GetCookies(Sender<Vec<cookie::Cookie<'static>>>, String),
|
||||
Jni(Box<dyn FnOnce(&mut JNIEnv, &JObject, &JObject) + Send>),
|
||||
LoadUrl(String, Option<http::HeaderMap>),
|
||||
LoadHtml(String),
|
||||
Reload,
|
||||
ClearAllBrowsingData,
|
||||
OnDestroy {
|
||||
activity_id: ActivityId,
|
||||
webview_id: WebviewId,
|
||||
is_changing_configurations: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CreateWebViewAttributes {
|
||||
pub id: String,
|
||||
pub url: Option<String>,
|
||||
pub html: Option<String>,
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
pub devtools: bool,
|
||||
pub transparent: bool,
|
||||
pub background_color: Option<RGBA>,
|
||||
pub headers: Option<http::HeaderMap>,
|
||||
pub autoplay: bool,
|
||||
pub on_webview_created:
|
||||
Option<Arc<dyn Fn(super::Context) -> JniResult<()> + Send + Sync + 'static>>,
|
||||
pub user_agent: Option<String>,
|
||||
pub initialization_scripts: Vec<InitializationScript>,
|
||||
pub javascript_disabled: bool,
|
||||
}
|
||||
|
||||
// SAFETY: only use this when you are sure the span will be dropped on the same thread it was entered
|
||||
#[cfg(feature = "tracing")]
|
||||
struct SendEnteredSpan(tracing::span::EnteredSpan);
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
unsafe impl Send for SendEnteredSpan {}
|
||||
|
|
@ -0,0 +1,530 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{PageLoadEvent, WebViewAttributes, RGBA};
|
||||
use crate::{
|
||||
custom_protocol_workaround, inject_initialization_scripts::inject_scripts_into_html, Error,
|
||||
RequestAsyncResponder, Result,
|
||||
};
|
||||
use crossbeam_channel::*;
|
||||
|
||||
use http::{Request, Response as HttpResponse};
|
||||
use jni::{
|
||||
errors::Result as JniResult,
|
||||
objects::{GlobalRef, JClass, JObject},
|
||||
JNIEnv,
|
||||
};
|
||||
use ndk::looper::ThreadLooper;
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use raw_window_handle::HasWindowHandle;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
sync::{mpsc::channel, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub(crate) mod binding;
|
||||
mod main_pipe;
|
||||
use main_pipe::{
|
||||
activity_id_for_window_manager, first_activity_id, register_activity_proxy, ActivityId,
|
||||
CreateWebViewAttributes, MainPipe, WebViewMessage,
|
||||
};
|
||||
|
||||
use crate::util::Counter;
|
||||
|
||||
static COUNTER: Counter = Counter::new();
|
||||
const MAIN_PIPE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct Context<'a, 'b> {
|
||||
pub env: &'a mut JNIEnv<'b>,
|
||||
pub activity: &'a JObject<'b>,
|
||||
pub webview: &'a JObject<'b>,
|
||||
}
|
||||
|
||||
type WebviewId = String;
|
||||
|
||||
macro_rules! define_static_handlers {
|
||||
($($key: ident, $var:ident = $type_name:ident);+ $(;)?) => {
|
||||
$(static $var: Lazy<Mutex<HashMap<$key, $type_name>>> = Lazy::new(||Mutex::new(HashMap::new()));)*
|
||||
};
|
||||
|
||||
($($var:ident = $type_name:ident { $($fields:ident:$types:ty),+ $(,)? });+ $(;)?) => {
|
||||
$(
|
||||
static $var: Lazy<Mutex<HashMap<WebviewId, $type_name>>> = Lazy::new(||Mutex::new(HashMap::new()));
|
||||
pub struct $type_name {
|
||||
$($fields: $types,)*
|
||||
}
|
||||
impl $type_name {
|
||||
pub fn new($($fields: $types,)*) -> Self {
|
||||
Self {
|
||||
$($fields,)*
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe impl Send for $type_name {}
|
||||
unsafe impl Sync for $type_name {})*
|
||||
};
|
||||
}
|
||||
|
||||
define_static_handlers! {
|
||||
IPC = UnsafeIpc { handler: Box<dyn Fn(Request<String>)> };
|
||||
REQUEST_HANDLER = UnsafeRequestHandler { handler: Box<dyn Fn(&str, Request<Vec<u8>>, bool) -> Option<HttpResponse<Cow<'static, [u8]>>>> };
|
||||
TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box<dyn Fn(String)> };
|
||||
URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box<dyn Fn(String) -> bool> };
|
||||
ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box<dyn Fn(PageLoadEvent, String)> };
|
||||
}
|
||||
define_static_handlers! {
|
||||
WebviewId, WITH_ASSET_LOADER = bool;
|
||||
WebviewId, ASSET_LOADER_DOMAIN = String;
|
||||
ActivityId, WEBVIEW_ATTRIBUTES = CreateWebViewAttributes;
|
||||
}
|
||||
|
||||
pub(crate) static PACKAGE: OnceCell<String> = OnceCell::new();
|
||||
|
||||
type EvalCallback = Box<dyn Fn(String) + Send + 'static>;
|
||||
|
||||
pub static EVAL_ID_GENERATOR: Counter = Counter::new();
|
||||
pub static EVAL_CALLBACKS: OnceCell<Mutex<HashMap<i32, EvalCallback>>> = OnceCell::new();
|
||||
|
||||
pub fn destroy_webview(activity_id: ActivityId, webview_id: &WebviewId) {
|
||||
WEBVIEW_ATTRIBUTES.lock().unwrap().remove(&activity_id);
|
||||
IPC.lock().unwrap().remove(webview_id);
|
||||
REQUEST_HANDLER.lock().unwrap().remove(webview_id);
|
||||
TITLE_CHANGE_HANDLER.lock().unwrap().remove(webview_id);
|
||||
URL_LOADING_OVERRIDE.lock().unwrap().remove(webview_id);
|
||||
ON_LOAD_HANDLER.lock().unwrap().remove(webview_id);
|
||||
WITH_ASSET_LOADER.lock().unwrap().remove(webview_id);
|
||||
ASSET_LOADER_DOMAIN.lock().unwrap().remove(webview_id);
|
||||
}
|
||||
|
||||
/// Sets up the necessary logic for wry to be able to create the webviews later.
|
||||
///
|
||||
/// This function must be run on the thread where the [`JNIEnv`] is registered and the looper is local,
|
||||
/// hence the requirement for a [`ThreadLooper`].
|
||||
pub unsafe fn android_setup(
|
||||
package: &str,
|
||||
mut env: JNIEnv,
|
||||
_looper: &ThreadLooper,
|
||||
activity: GlobalRef,
|
||||
) {
|
||||
PACKAGE.get_or_init(move || package.to_string());
|
||||
|
||||
let vm = env.get_java_vm().unwrap();
|
||||
|
||||
let activity_id = env
|
||||
.call_method(activity.as_obj(), "getId", "()I", &[])
|
||||
.unwrap()
|
||||
.i()
|
||||
.unwrap();
|
||||
|
||||
let window_manager = env
|
||||
.call_method(
|
||||
&activity,
|
||||
"getWindowManager",
|
||||
"()Landroid/view/WindowManager;",
|
||||
&[],
|
||||
)
|
||||
.unwrap()
|
||||
.l()
|
||||
.unwrap();
|
||||
let window_manager = env.new_global_ref(window_manager).unwrap();
|
||||
|
||||
// we must create the WebChromeClient here because it calls `registerForActivityResult`,
|
||||
// which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook
|
||||
let rust_webchrome_client_class = find_class(
|
||||
&mut env,
|
||||
activity.as_obj(),
|
||||
format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
let webchrome_client = env
|
||||
.new_object(
|
||||
&rust_webchrome_client_class,
|
||||
format!("(L{}/WryActivity;)V", PACKAGE.get().unwrap()),
|
||||
&[activity.as_obj().into()],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let webchrome_client = env.new_global_ref(webchrome_client).unwrap();
|
||||
|
||||
register_activity_proxy(vm, activity_id, activity, window_manager, webchrome_client);
|
||||
|
||||
if let Some(webview_attributes) = WEBVIEW_ATTRIBUTES.lock().unwrap().get(&activity_id) {
|
||||
MainPipe::send(
|
||||
activity_id,
|
||||
WebViewMessage::CreateWebView(webview_attributes.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InnerWebView {
|
||||
id: String,
|
||||
pub activity_id: ActivityId,
|
||||
}
|
||||
|
||||
impl InnerWebView {
|
||||
pub fn new_as_child(
|
||||
window: &impl HasWindowHandle,
|
||||
attributes: WebViewAttributes,
|
||||
pl_attrs: super::PlatformSpecificWebViewAttributes,
|
||||
) -> Result<Self> {
|
||||
Self::new(window, attributes, pl_attrs)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
window: &impl HasWindowHandle,
|
||||
attributes: WebViewAttributes,
|
||||
pl_attrs: super::PlatformSpecificWebViewAttributes,
|
||||
) -> Result<Self> {
|
||||
let window_manager = match window.window_handle()?.as_raw() {
|
||||
raw_window_handle::RawWindowHandle::AndroidNdk(window_manager) => {
|
||||
window_manager.a_native_window
|
||||
}
|
||||
_ => return Err(Error::UnsupportedWindowHandle),
|
||||
};
|
||||
let window_manager = unsafe { JObject::from_raw(window_manager.as_ptr().cast()) };
|
||||
let activity_id =
|
||||
activity_id_for_window_manager(window_manager).expect("no available activity");
|
||||
let WebViewAttributes {
|
||||
url,
|
||||
html,
|
||||
initialization_scripts,
|
||||
ipc_handler,
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
devtools,
|
||||
custom_protocols,
|
||||
background_color,
|
||||
transparent,
|
||||
headers,
|
||||
autoplay,
|
||||
user_agent,
|
||||
javascript_disabled,
|
||||
..
|
||||
} = attributes;
|
||||
|
||||
let super::PlatformSpecificWebViewAttributes {
|
||||
on_webview_created,
|
||||
with_asset_loader,
|
||||
asset_loader_domain,
|
||||
https_scheme,
|
||||
} = pl_attrs;
|
||||
|
||||
let http_or_https = if https_scheme { "https" } else { "http" };
|
||||
|
||||
let url = if let Some(mut url) = url {
|
||||
if let Some((protocol, _)) = url.split_once("://") {
|
||||
if custom_protocols.contains_key(protocol) {
|
||||
url = custom_protocol_workaround::apply_uri_work_around(&url, http_or_https, protocol)
|
||||
}
|
||||
}
|
||||
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let id = attributes
|
||||
.id
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_else(|| COUNTER.next().to_string());
|
||||
|
||||
WITH_ASSET_LOADER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.clone(), with_asset_loader);
|
||||
if let Some(domain) = asset_loader_domain {
|
||||
ASSET_LOADER_DOMAIN
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.clone(), domain);
|
||||
}
|
||||
|
||||
let initialization_scripts_ = initialization_scripts.clone();
|
||||
REQUEST_HANDLER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(
|
||||
id.clone(),
|
||||
UnsafeRequestHandler::new(Box::new(
|
||||
move |webview_id: &str, mut request, is_document_start_script_enabled| {
|
||||
let uri = request.uri().to_string();
|
||||
if let Some((custom_protocol, custom_protocol_handler)) =
|
||||
custom_protocols.iter().find(|(protocol, _)| {
|
||||
custom_protocol_workaround::is_work_around_uri(&uri, http_or_https, protocol)
|
||||
})
|
||||
{
|
||||
let uri_res = custom_protocol_workaround::revert_uri_work_around(
|
||||
&uri,
|
||||
http_or_https,
|
||||
custom_protocol,
|
||||
)
|
||||
.parse();
|
||||
|
||||
if let Ok(uri) = uri_res {
|
||||
*request.uri_mut() = uri;
|
||||
}
|
||||
|
||||
let (tx, rx) = channel();
|
||||
let initialization_scripts = initialization_scripts_.clone();
|
||||
let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
|
||||
Box::new(move |mut response| {
|
||||
if !is_document_start_script_enabled {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("`addDocumentStartJavaScript` is not supported; injecting initialization scripts via custom protocol handler");
|
||||
response = inject_scripts_into_html(response, &initialization_scripts);
|
||||
}
|
||||
let _ = tx.send(response);
|
||||
});
|
||||
|
||||
(custom_protocol_handler)(webview_id, request, RequestAsyncResponder { responder });
|
||||
// 3x the timeout while we monitor https://github.com/tauri-apps/wry/issues/1551
|
||||
// TODO: Remove timeout
|
||||
return rx.recv_timeout(MAIN_PIPE_TIMEOUT * 3).inspect_err(|e| {eprintln!("custom protocol timed out: {e}");}).ok();
|
||||
}
|
||||
None
|
||||
},
|
||||
)));
|
||||
|
||||
if let Some(i) = ipc_handler {
|
||||
IPC
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.clone(), UnsafeIpc::new(Box::new(i)));
|
||||
}
|
||||
|
||||
if let Some(i) = attributes.document_title_changed_handler {
|
||||
TITLE_CHANGE_HANDLER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.clone(), UnsafeTitleHandler::new(i));
|
||||
}
|
||||
|
||||
if let Some(i) = attributes.navigation_handler {
|
||||
URL_LOADING_OVERRIDE
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.clone(), UnsafeUrlLoadingOverride::new(i));
|
||||
}
|
||||
|
||||
if let Some(h) = attributes.on_page_load_handler {
|
||||
ON_LOAD_HANDLER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.clone(), UnsafeOnPageLoadHandler::new(h));
|
||||
}
|
||||
|
||||
let attributes = CreateWebViewAttributes {
|
||||
id: id.clone(),
|
||||
url,
|
||||
html,
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
devtools,
|
||||
background_color,
|
||||
transparent,
|
||||
headers,
|
||||
on_webview_created,
|
||||
autoplay,
|
||||
user_agent,
|
||||
initialization_scripts,
|
||||
javascript_disabled,
|
||||
};
|
||||
|
||||
WEBVIEW_ATTRIBUTES
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(activity_id, attributes.clone());
|
||||
|
||||
MainPipe::send(activity_id, WebViewMessage::CreateWebView(attributes));
|
||||
|
||||
Ok(Self { id, activity_id })
|
||||
}
|
||||
|
||||
pub fn print(&self) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn id(&self) -> crate::WebViewId<'_> {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn url(&self) -> crate::Result<String> {
|
||||
let (tx, rx) = bounded(1);
|
||||
MainPipe::send(self.activity_id, WebViewMessage::GetUrl(tx));
|
||||
rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn eval(&self, js: &str, callback: Option<impl Fn(String) + Send + 'static>) -> Result<()> {
|
||||
MainPipe::send(
|
||||
self.activity_id,
|
||||
WebViewMessage::Eval(
|
||||
js.into(),
|
||||
callback.map(|c| Box::new(c) as Box<dyn Fn(String) + Send + 'static>),
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
pub fn open_devtools(&self) {}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
pub fn close_devtools(&self) {}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
pub fn is_devtools_open(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn zoom(&self, _scale_factor: f64) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_background_color(&self, background_color: RGBA) -> Result<()> {
|
||||
MainPipe::send(
|
||||
self.activity_id,
|
||||
WebViewMessage::SetBackgroundColor(background_color),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_url(&self, url: &str) -> Result<()> {
|
||||
MainPipe::send(
|
||||
self.activity_id,
|
||||
WebViewMessage::LoadUrl(url.to_string(), None),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) -> Result<()> {
|
||||
MainPipe::send(
|
||||
self.activity_id,
|
||||
WebViewMessage::LoadUrl(url.to_string(), Some(headers)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_html(&self, html: &str) -> Result<()> {
|
||||
MainPipe::send(self.activity_id, WebViewMessage::LoadHtml(html.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reload(&self) -> Result<()> {
|
||||
MainPipe::send(self.activity_id, WebViewMessage::Reload);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_all_browsing_data(&self) -> Result<()> {
|
||||
MainPipe::send(self.activity_id, WebViewMessage::ClearAllBrowsingData);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
|
||||
let (tx, rx) = bounded(1);
|
||||
MainPipe::send(
|
||||
self.activity_id,
|
||||
WebViewMessage::GetCookies(tx, url.to_string()),
|
||||
);
|
||||
rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn set_cookie(&self, #[allow(unused)] cookie: &cookie::Cookie<'_>) -> Result<()> {
|
||||
// Unsupported
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_cookie(&self, #[allow(unused)] cookie: &cookie::Cookie<'_>) -> Result<()> {
|
||||
// Unsupported
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub fn bounds(&self) -> Result<crate::Rect> {
|
||||
Ok(crate::Rect::default())
|
||||
}
|
||||
|
||||
pub fn set_bounds(&self, _bounds: crate::Rect) -> Result<()> {
|
||||
// Unsupported
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_visible(&self, _visible: bool) -> Result<()> {
|
||||
// Unsupported
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn focus(&self) -> Result<()> {
|
||||
// Unsupported
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn focus_parent(&self) -> Result<()> {
|
||||
// Unsupported
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct JniHandle {
|
||||
pub(crate) activity_id: ActivityId,
|
||||
}
|
||||
|
||||
impl JniHandle {
|
||||
/// Execute jni code on the thread of the webview.
|
||||
/// Provided function will be provided with the jni evironment, Android activity and WebView
|
||||
pub fn exec<F>(&self, func: F)
|
||||
where
|
||||
F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static,
|
||||
{
|
||||
MainPipe::send(self.activity_id, WebViewMessage::Jni(Box::new(func)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn platform_webview_version() -> Result<String> {
|
||||
let (tx, rx) = bounded(1);
|
||||
let activity_id = loop {
|
||||
match first_activity_id() {
|
||||
Some(id) => break id,
|
||||
None => {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
};
|
||||
MainPipe::send(activity_id, WebViewMessage::GetWebViewVersion(tx));
|
||||
rx.recv_timeout(MAIN_PIPE_TIMEOUT)?
|
||||
}
|
||||
|
||||
/// Finds a class in the project scope.
|
||||
pub fn find_class<'a>(
|
||||
env: &mut JNIEnv<'a>,
|
||||
activity: &JObject<'_>,
|
||||
name: String,
|
||||
) -> JniResult<JClass<'a>> {
|
||||
let class_name = env.new_string(name.replace('/', "."))?;
|
||||
let my_class = env
|
||||
.call_method(
|
||||
activity,
|
||||
"getAppClass",
|
||||
"(Ljava/lang/String;)Ljava/lang/Class;",
|
||||
&[(&class_name).into()],
|
||||
)?
|
||||
.l()?;
|
||||
Ok(my_class.into())
|
||||
}
|
||||
|
||||
/// Dispatch a closure to run on the Android context.
|
||||
///
|
||||
/// The closure takes the JNI env, the Android activity instance and the possibly null webview.
|
||||
pub fn dispatch<F>(func: F)
|
||||
where
|
||||
F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static,
|
||||
{
|
||||
MainPipe::send(
|
||||
first_activity_id().expect("no available activity"),
|
||||
WebViewMessage::Jni(Box::new(func)),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
//! - WebView2 supports non-standard protocols only on Windows 10+, so we have to use a workaround.
|
||||
//! See <https://github.com/MicrosoftEdge/WebView2Feedback/issues/73>
|
||||
//! - On Android, there's no API for registering custom protocols, so this workaround is also used.
|
||||
//!
|
||||
//! The process looks like this:
|
||||
//!
|
||||
//! 1. Use [`apply_uri_work_around`] to convert the URI we want to navigate to
|
||||
//! 2. Intercept http(s) requests, test the request URI against [`is_work_around_uri`],
|
||||
//! if it matches, we apply [`revert_uri_work_around`] to the URI and feed it to the custom protocol handler
|
||||
|
||||
/// If the URI is a work around URI for this protocol which starts with `{http_or_https}://{protocol}.`
|
||||
pub fn is_work_around_uri(uri: &str, http_or_https: &str, protocol: &str) -> bool {
|
||||
uri
|
||||
.strip_prefix(http_or_https)
|
||||
.and_then(|rest| rest.strip_prefix("://"))
|
||||
.and_then(|rest| rest.strip_prefix(protocol))
|
||||
.and_then(|rest| rest.strip_prefix("."))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// Conveting `{protocol}://localhost/abc` to `{http_or_https}://{protocol}.localhost/abc`
|
||||
pub fn apply_uri_work_around(uri: &str, http_or_https: &str, protocol: &str) -> String {
|
||||
uri.replace(
|
||||
&original_uri_prefix(protocol),
|
||||
&work_around_uri_prefix(http_or_https, protocol),
|
||||
)
|
||||
}
|
||||
|
||||
/// Conveting `{http_or_https}://{protocol}.localhost/abc` back to `{protocol}://localhost/abc`
|
||||
pub fn revert_uri_work_around(uri: &str, http_or_https: &str, protocol: &str) -> String {
|
||||
uri.replace(
|
||||
&work_around_uri_prefix(http_or_https, protocol),
|
||||
&original_uri_prefix(protocol),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn original_uri_prefix(protocol: &str) -> String {
|
||||
format!("{protocol}://")
|
||||
}
|
||||
|
||||
pub fn work_around_uri_prefix(http_or_https: &str, protocol: &str) -> String {
|
||||
format!("{http_or_https}://{protocol}.")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_work_around_uri;
|
||||
|
||||
#[test]
|
||||
fn checks_if_custom_protocol_uri() {
|
||||
let scheme = "http";
|
||||
let uri = "http://wry.localhost/path/to/page";
|
||||
assert!(is_work_around_uri(uri, scheme, "wry"));
|
||||
assert!(!is_work_around_uri(uri, scheme, "asset"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/// Convenient type alias of Result type for wry.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Errors returned by wry.
|
||||
#[non_exhaustive]
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[cfg(gtk)]
|
||||
#[error(transparent)]
|
||||
GlibError(#[from] gtk::glib::Error),
|
||||
#[cfg(gtk)]
|
||||
#[error(transparent)]
|
||||
GlibBoolError(#[from] gtk::glib::BoolError),
|
||||
#[cfg(gtk)]
|
||||
#[error("Fail to fetch security manager")]
|
||||
MissingManager,
|
||||
#[cfg(gtk)]
|
||||
#[error("Couldn't find X11 Display")]
|
||||
X11DisplayNotFound,
|
||||
#[cfg(all(gtk, feature = "x11"))]
|
||||
#[error(transparent)]
|
||||
XlibError(#[from] x11_dl::error::OpenError),
|
||||
#[error("Failed to initialize the script")]
|
||||
InitScriptError,
|
||||
#[error("Bad RPC request: {0} ((1))")]
|
||||
RpcScriptError(String, String),
|
||||
#[error(transparent)]
|
||||
NulError(#[from] std::ffi::NulError),
|
||||
#[error(transparent)]
|
||||
ReceiverError(#[from] std::sync::mpsc::RecvError),
|
||||
#[cfg(target_os = "android")]
|
||||
#[error(transparent)]
|
||||
ReceiverTimeoutError(#[from] crossbeam_channel::RecvTimeoutError),
|
||||
#[error(transparent)]
|
||||
SenderError(#[from] std::sync::mpsc::SendError<String>),
|
||||
#[error("Failed to send the message")]
|
||||
MessageSender,
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[cfg(target_os = "windows")]
|
||||
#[error("WebView2 error: {0}")]
|
||||
WebView2Error(webview2_com::Error),
|
||||
#[error(transparent)]
|
||||
HttpError(#[from] http::Error),
|
||||
#[error("Infallible error, something went really wrong: {0}")]
|
||||
Infallible(#[from] std::convert::Infallible),
|
||||
#[cfg(target_os = "android")]
|
||||
#[error(transparent)]
|
||||
JniError(#[from] jni::errors::Error),
|
||||
#[error("Failed to create proxy endpoint")]
|
||||
ProxyEndpointCreationFailed,
|
||||
#[error(transparent)]
|
||||
WindowHandleError(#[from] raw_window_handle::HandleError),
|
||||
#[error("the window handle kind is not supported")]
|
||||
UnsupportedWindowHandle,
|
||||
#[error(transparent)]
|
||||
Utf8Error(#[from] std::str::Utf8Error),
|
||||
#[cfg(target_os = "android")]
|
||||
#[error(transparent)]
|
||||
CrossBeamRecvError(#[from] crossbeam_channel::RecvError),
|
||||
#[error("not on the main thread")]
|
||||
NotMainThread,
|
||||
#[error("Custom protocol task is invalid.")]
|
||||
CustomProtocolTaskInvalid,
|
||||
#[error("Failed to register URL scheme: {0}, could be due to invalid URL scheme or the scheme is already registered.")]
|
||||
UrlSchemeRegisterError(String),
|
||||
#[error("Duplicate custom protocol '{0}' registered on the WebViewBuilder")]
|
||||
DuplicateCustomProtocol(String),
|
||||
#[error("Duplicate custom protocol '{0}' registered on the same web context on Linux")]
|
||||
ContextDuplicateCustomProtocol(String),
|
||||
#[error(transparent)]
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
#[error("data store is currently opened")]
|
||||
DataStoreInUse,
|
||||
#[cfg(target_os = "android")]
|
||||
#[error("Activity not found")]
|
||||
ActivityNotFound,
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! This is an internal implementation detail used by the Android backend to inject
|
||||
//! initialization scripts when `addDocumentStartJavaScript` is not supported.
|
||||
|
||||
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||
use dom_query::Document;
|
||||
use http::{
|
||||
header::{HeaderValue, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
|
||||
Response as HttpResponse,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::InitializationScript;
|
||||
|
||||
pub fn inject_scripts_into_html(
|
||||
mut response: HttpResponse<Cow<'static, [u8]>>,
|
||||
scripts: &[InitializationScript],
|
||||
) -> HttpResponse<Cow<'static, [u8]>> {
|
||||
if scripts.is_empty() {
|
||||
return response;
|
||||
}
|
||||
|
||||
let should_inject_scripts = response
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
// Content-Type must begin with the media type, but is case-insensitive.
|
||||
// It may also be followed by any number of semicolon-delimited key value pairs.
|
||||
// We don't care about these here.
|
||||
// source: https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.1
|
||||
.and_then(|content_type| content_type.to_str().ok())
|
||||
.map(|content_type_str| content_type_str.to_lowercase().starts_with("text/html"))
|
||||
.unwrap_or_default();
|
||||
|
||||
if !should_inject_scripts {
|
||||
return response;
|
||||
}
|
||||
|
||||
let document = Document::from(String::from_utf8_lossy(response.body()).as_ref());
|
||||
let csp = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY);
|
||||
|
||||
// Get or create head element
|
||||
let head = document.head().unwrap_or_else(|| {
|
||||
let html = document.html_root();
|
||||
let head = document.tree.new_element("head");
|
||||
html.prepend_child(&head);
|
||||
head
|
||||
});
|
||||
|
||||
// Iterate in reverse order since we are prepending each script to the head tag
|
||||
let mut hashes = Vec::new();
|
||||
for script in scripts.iter().rev().map(|s| &s.script) {
|
||||
let script_tag = document.tree.new_element("script");
|
||||
script_tag.set_text(script.as_str());
|
||||
head.prepend_child(&script_tag);
|
||||
if csp.is_some() {
|
||||
hashes.push(hash_script(script));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(csp) = csp {
|
||||
let csp_string = csp.to_str().unwrap().to_string();
|
||||
let csp_string = if csp_string.contains("script-src") {
|
||||
csp_string.replace("script-src", &format!("script-src {}", hashes.join(" ")))
|
||||
} else {
|
||||
format!("{csp_string} script-src {}", hashes.join(" "))
|
||||
};
|
||||
*csp = HeaderValue::from_str(&csp_string).unwrap();
|
||||
}
|
||||
|
||||
*response.body_mut() = Cow::Owned(document.html().as_bytes().to_vec());
|
||||
response
|
||||
}
|
||||
|
||||
fn hash_script(script: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(script);
|
||||
let hash = hasher.finalize();
|
||||
format!("'sha256-{}'", BASE64_STANDARD.encode(hash))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::StatusCode;
|
||||
|
||||
#[test]
|
||||
fn test_no_scripts_returns_original_response() {
|
||||
let body = "<html><head></head><body>Test</body></html>";
|
||||
|
||||
let result = run(body, "text/html", vec![]);
|
||||
|
||||
assert_eq!(result, body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_html_response_not_modified() {
|
||||
let body = r#"{"key": "value"}"#;
|
||||
let scripts = vec!["console.log('test');".to_string()];
|
||||
|
||||
let result = run(body, "application/json", scripts);
|
||||
|
||||
assert_eq!(result, body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_single_script() {
|
||||
let body = "<html><head></head><body>Content</body></html>";
|
||||
let scripts = vec!["console.log('injected');".to_string()];
|
||||
|
||||
let result = run(body, "text/html", scripts);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
"<html><head><script>console.log('injected');</script></head><body>Content</body></html>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_multiple_scripts() {
|
||||
let body = "<html><head></head><body>Content</body></html>";
|
||||
let scripts = vec![
|
||||
"var first = 1;".to_owned(),
|
||||
"let second = 2;".to_owned(),
|
||||
"const third = 3;".to_owned(),
|
||||
"window.test = () => console.log('test');".to_owned(),
|
||||
];
|
||||
|
||||
let result = run(body, "text/html", scripts);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
"<html><head><script>var first = 1;</script><script>let second = 2;</script><script>const third = 3;</script><script>window.test = () => console.log('test');</script></head><body>Content</body></html>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_script_creates_head_if_missing() {
|
||||
let body = "<html><body>Content</body></html>";
|
||||
let scripts = vec!["console.log('test');".to_string()];
|
||||
|
||||
let result = run(body, "text/html", scripts);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
"<html><head><script>console.log('test');</script></head><body>Content</body></html>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_script_creates_html_structure_if_missing() {
|
||||
let body = "Just some text";
|
||||
let scripts = vec!["console.log('test');".to_string()];
|
||||
|
||||
let result = run(body, "text/html", scripts);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
"<html><head><script>console.log('test');</script></head><body>Just some text</body></html>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_csp_header_updated_with_script_hashes() {
|
||||
let body = "<html><head></head><body>Content</body></html>";
|
||||
let mut response = create_response(body, "text/html");
|
||||
response.headers_mut().insert(
|
||||
CONTENT_SECURITY_POLICY,
|
||||
HeaderValue::from_static("default-src 'self'"),
|
||||
);
|
||||
|
||||
let script_code = "console.log('test');";
|
||||
let scripts = vec![script_code.to_string()];
|
||||
|
||||
let scripts: Vec<InitializationScript> = scripts
|
||||
.into_iter()
|
||||
.map(|script| InitializationScript {
|
||||
script,
|
||||
for_main_frame_only: true,
|
||||
})
|
||||
.collect();
|
||||
let result = inject_scripts_into_html(response, &scripts);
|
||||
let result_body = String::from_utf8_lossy(result.body()).to_string();
|
||||
let csp = result.headers().get(CONTENT_SECURITY_POLICY).unwrap();
|
||||
let csp_str = csp.to_str().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result_body,
|
||||
"<html><head><script>console.log('test');</script></head><body>Content</body></html>"
|
||||
);
|
||||
assert_eq!(
|
||||
csp_str,
|
||||
"default-src 'self' script-src 'sha256-3x8DE279hr8o/Aq0dEdH4WApIwn5rbRKhugPzn6Bofw='"
|
||||
);
|
||||
}
|
||||
|
||||
fn create_response(body: &str, content_type: &'static str) -> HttpResponse<Cow<'static, [u8]>> {
|
||||
let mut response = HttpResponse::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Cow::Owned(body.as_bytes().to_vec()))
|
||||
.unwrap();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
/// Helper function to create a response, inject scripts, and return the body as a string
|
||||
fn run(body: &str, content_type: &'static str, scripts: Vec<String>) -> String {
|
||||
let response = create_response(body, content_type);
|
||||
let scripts: Vec<InitializationScript> = scripts
|
||||
.into_iter()
|
||||
.map(|script| InitializationScript {
|
||||
script,
|
||||
for_main_frame_only: true,
|
||||
})
|
||||
.collect();
|
||||
let result = inject_scripts_into_html(response, &scripts);
|
||||
String::from_utf8_lossy(result.body()).to_string()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,15 @@
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyEndpoint {
|
||||
/// Proxy server host (e.g. 192.168.0.100, localhost, example.com, etc.)
|
||||
pub host: String,
|
||||
/// Proxy server port (e.g. 1080, 3128, etc.)
|
||||
pub port: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProxyConfig {
|
||||
/// Connect to proxy server via HTTP CONNECT
|
||||
Http(ProxyEndpoint),
|
||||
/// Connect to proxy server via SOCKSv5
|
||||
Socks5(ProxyEndpoint),
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
pub struct Counter(AtomicU32);
|
||||
|
||||
impl Counter {
|
||||
pub const fn new() -> Self {
|
||||
Self(AtomicU32::new(1))
|
||||
}
|
||||
|
||||
pub fn next(&self) -> u32 {
|
||||
self.0.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(gtk)]
|
||||
use crate::webkitgtk::WebContextImpl;
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// A context that is shared between multiple [`WebView`]s.
|
||||
///
|
||||
/// A browser would have a context for all the normal tabs and a different context for all the
|
||||
/// private/incognito tabs.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// If [`WebView`] is created by a WebContext. Dropping `WebContext` will cause [`WebView`] lose
|
||||
/// some actions like custom protocol on Mac. Please keep both instances when you still wish to
|
||||
/// interact with them.
|
||||
///
|
||||
/// [`WebView`]: crate::WebView
|
||||
#[derive(Debug)]
|
||||
pub struct WebContext {
|
||||
data_directory: Option<PathBuf>,
|
||||
#[allow(dead_code)] // It's not needed on Windows and macOS.
|
||||
pub(crate) os: WebContextImpl,
|
||||
#[allow(dead_code)] // It's not needed on Windows and macOS.
|
||||
pub(crate) custom_protocols: HashSet<String>,
|
||||
}
|
||||
|
||||
impl WebContext {
|
||||
/// Create a new [`WebContext`].
|
||||
///
|
||||
/// - `data_directory`: Whether the WebView window should have a custom user data path.
|
||||
/// This is useful in Windows when a bundled application can't have the webview data inside `Program Files`.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows**: Webview instances with different `CoreWebView2EnvironmentOptions` must have different `data_directory`s [^1]
|
||||
///
|
||||
/// [^1]: <https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2environment.createcorewebview2controllerasync?view=webview2-dotnet-1.0.3719.77#:~:text=WebView%20creation%20fails%20if%20a%20running%20instance%20using%20the%20same%20user%20data%20folder%20exists%2C%20and%20the%20Environment%20objects%20have%20different%20CoreWebView2EnvironmentOptions.>
|
||||
pub fn new(data_directory: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
os: WebContextImpl::new(data_directory.as_deref()),
|
||||
data_directory,
|
||||
custom_protocols: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(gtk)]
|
||||
pub(crate) fn new_ephemeral() -> Self {
|
||||
Self {
|
||||
os: WebContextImpl::new_ephemeral(),
|
||||
data_directory: None,
|
||||
custom_protocols: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to the data directory the context was created with.
|
||||
pub fn data_directory(&self) -> Option<&Path> {
|
||||
self.data_directory.as_deref()
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd",
|
||||
))]
|
||||
pub(crate) fn register_custom_protocol(&mut self, name: String) -> Result<(), crate::Error> {
|
||||
if self.is_custom_protocol_registered(&name) {
|
||||
return Err(crate::Error::ContextDuplicateCustomProtocol(name));
|
||||
}
|
||||
self.custom_protocols.insert(name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a custom protocol has been registered on this context.
|
||||
pub fn is_custom_protocol_registered(&self, name: &str) -> bool {
|
||||
self.custom_protocols.contains(name)
|
||||
}
|
||||
|
||||
/// Set if this context allows automation.
|
||||
///
|
||||
/// **Note:** This is currently only enforced on Linux, and has the stipulation that
|
||||
/// only 1 context allows automation at a time.
|
||||
pub fn set_allows_automation(&mut self, flag: bool) {
|
||||
self.os.set_allows_automation(flag);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WebContext {
|
||||
fn default() -> Self {
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(gtk))]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WebContextImpl;
|
||||
|
||||
#[cfg(not(gtk))]
|
||||
impl WebContextImpl {
|
||||
fn new(_: Option<&Path>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn set_allows_automation(&mut self, _flag: bool) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
cell::{Cell, UnsafeCell},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use gtk::{glib::GString, prelude::*};
|
||||
use webkit2gtk::WebView;
|
||||
|
||||
use crate::DragDropEvent;
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
|
||||
enum DragControllerState {
|
||||
Entered,
|
||||
Leaving,
|
||||
Left,
|
||||
}
|
||||
|
||||
struct DragDropController {
|
||||
paths: UnsafeCell<Option<Vec<PathBuf>>>,
|
||||
state: Cell<DragControllerState>,
|
||||
position: Cell<(i32, i32)>,
|
||||
handler: Box<dyn Fn(DragDropEvent) -> bool>,
|
||||
}
|
||||
|
||||
impl DragDropController {
|
||||
fn new(handler: Box<dyn Fn(DragDropEvent) -> bool>) -> Self {
|
||||
Self {
|
||||
handler,
|
||||
paths: UnsafeCell::new(None),
|
||||
state: Cell::new(DragControllerState::Left),
|
||||
position: Cell::new((0, 0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn store_paths(&self, paths: Vec<PathBuf>) {
|
||||
unsafe { *self.paths.get() = Some(paths) };
|
||||
}
|
||||
|
||||
fn take_paths(&self) -> Option<Vec<PathBuf>> {
|
||||
unsafe { &mut *self.paths.get() }.take()
|
||||
}
|
||||
|
||||
fn store_position(&self, position: (i32, i32)) {
|
||||
self.position.replace(position);
|
||||
}
|
||||
|
||||
fn enter(&self) {
|
||||
self.state.set(DragControllerState::Entered);
|
||||
}
|
||||
|
||||
fn leaving(&self) {
|
||||
self.state.set(DragControllerState::Leaving);
|
||||
}
|
||||
|
||||
fn leave(&self) {
|
||||
self.state.set(DragControllerState::Left);
|
||||
}
|
||||
|
||||
fn state(&self) -> DragControllerState {
|
||||
self.state.get()
|
||||
}
|
||||
|
||||
fn call(&self, event: DragDropEvent) -> bool {
|
||||
(self.handler)(event)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn connect_drag_event(webview: &WebView, handler: Box<dyn Fn(DragDropEvent) -> bool>) {
|
||||
let controller = Rc::new(DragDropController::new(handler));
|
||||
|
||||
{
|
||||
let controller = controller.clone();
|
||||
webview.connect_drag_data_received(move |_, _, _, _, data, info, _| {
|
||||
if info == 2 {
|
||||
let uris = data.uris();
|
||||
let paths = uris.iter().map(path_buf_from_uri).collect::<Vec<_>>();
|
||||
controller.enter();
|
||||
controller.call(DragDropEvent::Enter {
|
||||
paths: paths.clone(),
|
||||
position: controller.position.get(),
|
||||
});
|
||||
controller.store_paths(paths);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let controller = controller.clone();
|
||||
webview.connect_drag_motion(move |_, _, x, y, _| {
|
||||
if controller.state() == DragControllerState::Entered {
|
||||
controller.call(DragDropEvent::Over { position: (x, y) });
|
||||
} else {
|
||||
controller.store_position((x, y));
|
||||
}
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let controller = controller.clone();
|
||||
webview.connect_drag_drop(move |_, ctx, x, y, time| {
|
||||
if controller.state() == DragControllerState::Leaving {
|
||||
if let Some(paths) = controller.take_paths() {
|
||||
ctx.drop_finish(true, time);
|
||||
controller.leave();
|
||||
return controller.call(DragDropEvent::Drop {
|
||||
paths,
|
||||
position: (x, y),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
webview.connect_drag_leave(move |_w, _, _| {
|
||||
if controller.state() != DragControllerState::Left {
|
||||
controller.leaving();
|
||||
let controller = controller.clone();
|
||||
gtk::glib::idle_add_local_once(move || {
|
||||
if controller.state() == DragControllerState::Leaving {
|
||||
controller.leave();
|
||||
controller.call(DragDropEvent::Leave);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn path_buf_from_uri(gstr: &GString) -> PathBuf {
|
||||
let path = gstr.as_str();
|
||||
let path = path.strip_prefix("file://").unwrap_or(path);
|
||||
let path = percent_encoding::percent_decode(path.as_bytes())
|
||||
.decode_utf8_lossy()
|
||||
.to_string();
|
||||
PathBuf::from(path)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,187 @@
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gtk::{
|
||||
gdk::{EventButton, EventMask, ModifierType},
|
||||
prelude::*,
|
||||
};
|
||||
use webkit2gtk::{WebView, WebViewExt};
|
||||
|
||||
pub fn setup(webview: &WebView) {
|
||||
webview.add_events(EventMask::BUTTON1_MOTION_MASK | EventMask::BUTTON_PRESS_MASK);
|
||||
|
||||
let bf_state = BackForwardState(Rc::new(RefCell::new(0)));
|
||||
|
||||
let bf_state_c = bf_state.clone();
|
||||
webview.connect_button_press_event(move |webview, event| {
|
||||
let mut inhibit = false;
|
||||
match event.button() {
|
||||
// back button
|
||||
8 => {
|
||||
inhibit = true;
|
||||
bf_state_c.set(BACK);
|
||||
webview.run_javascript(
|
||||
&create_js_mouse_event(event, true, &bf_state_c),
|
||||
None::<>k::gio::Cancellable>,
|
||||
|_| {},
|
||||
);
|
||||
}
|
||||
// forward button
|
||||
9 => {
|
||||
inhibit = true;
|
||||
bf_state_c.set(FORWARD);
|
||||
webview.run_javascript(
|
||||
&create_js_mouse_event(event, true, &bf_state_c),
|
||||
None::<>k::gio::Cancellable>,
|
||||
|_| {},
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if inhibit {
|
||||
gtk::glib::Propagation::Stop
|
||||
} else {
|
||||
gtk::glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
|
||||
let bf_state_c = bf_state.clone();
|
||||
webview.connect_button_release_event(move |webview, event| {
|
||||
let mut inhibit = false;
|
||||
match event.button() {
|
||||
// back button
|
||||
8 => {
|
||||
inhibit = true;
|
||||
bf_state_c.remove(BACK);
|
||||
webview.run_javascript(
|
||||
&create_js_mouse_event(event, false, &bf_state_c),
|
||||
None::<>k::gio::Cancellable>,
|
||||
|_| {},
|
||||
);
|
||||
}
|
||||
// forward button
|
||||
9 => {
|
||||
inhibit = true;
|
||||
bf_state_c.remove(FORWARD);
|
||||
webview.run_javascript(
|
||||
&create_js_mouse_event(event, false, &bf_state_c),
|
||||
None::<>k::gio::Cancellable>,
|
||||
|_| {},
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if inhibit {
|
||||
gtk::glib::Propagation::Stop
|
||||
} else {
|
||||
gtk::glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn create_js_mouse_event(event: &EventButton, pressed: bool, state: &BackForwardState) -> String {
|
||||
let event_name = if pressed { "mousedown" } else { "mouseup" };
|
||||
// js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
|
||||
let button = if event.button() == 8 { 3 } else { 4 };
|
||||
let (x, y) = event.position();
|
||||
let (x, y) = (x as i32, y as i32);
|
||||
let modifers_state = event.state();
|
||||
let mut buttons = 0;
|
||||
// left button
|
||||
if modifers_state.contains(ModifierType::BUTTON1_MASK) {
|
||||
buttons += 1;
|
||||
}
|
||||
// right button
|
||||
if modifers_state.contains(ModifierType::BUTTON3_MASK) {
|
||||
buttons += 2;
|
||||
}
|
||||
// middle button
|
||||
if modifers_state.contains(ModifierType::BUTTON2_MASK) {
|
||||
buttons += 4;
|
||||
}
|
||||
// back button
|
||||
if state.has(BACK) {
|
||||
buttons += 8;
|
||||
}
|
||||
// if modifers_state.contains(ModifierType::BUTTON4_MASK) {
|
||||
// buttons += 8;
|
||||
// }
|
||||
// forward button
|
||||
if state.has(FORWARD) {
|
||||
buttons += 16;
|
||||
}
|
||||
// if modifers_state.contains(ModifierType::BUTTON5_MASK) {
|
||||
// buttons += 16;
|
||||
// }
|
||||
format!(
|
||||
r#"(() => {{
|
||||
const el = document.elementFromPoint({x},{y});
|
||||
const ev = new MouseEvent('{event_name}', {{
|
||||
view: window,
|
||||
button: {button},
|
||||
buttons: {buttons},
|
||||
x: {x},
|
||||
y: {y},
|
||||
bubbles: true,
|
||||
detail: {detail},
|
||||
cancelBubble: false,
|
||||
cancelable: true,
|
||||
clientX: {x},
|
||||
clientY: {y},
|
||||
composed: true,
|
||||
layerX: {x},
|
||||
layerY: {y},
|
||||
pageX: {x},
|
||||
pageY: {y},
|
||||
screenX: window.screenX + {x},
|
||||
screenY: window.screenY + {y},
|
||||
ctrlKey: {ctrl_key},
|
||||
metaKey: {meta_key},
|
||||
shiftKey: {shift_key},
|
||||
altKey: {alt_key},
|
||||
}});
|
||||
el.dispatchEvent(ev)
|
||||
if (!ev.defaultPrevented && "{event_name}" === "mouseup") {{
|
||||
if (ev.button === 3) {{
|
||||
window.history.back();
|
||||
}}
|
||||
if (ev.button === 4) {{
|
||||
window.history.forward();
|
||||
}}
|
||||
}}
|
||||
}})()"#,
|
||||
event_name = event_name,
|
||||
x = x,
|
||||
y = y,
|
||||
detail = event.click_count().unwrap_or(1),
|
||||
ctrl_key = modifers_state.contains(ModifierType::CONTROL_MASK),
|
||||
alt_key = modifers_state.contains(ModifierType::MOD1_MASK),
|
||||
shift_key = modifers_state.contains(ModifierType::SHIFT_MASK),
|
||||
meta_key = modifers_state.contains(ModifierType::SUPER_MASK),
|
||||
button = button,
|
||||
buttons = buttons,
|
||||
)
|
||||
}
|
||||
|
||||
// Internal modifiers to track whether BACK/FORWARD buttons are pressed
|
||||
const BACK: u8 = 0b01;
|
||||
const FORWARD: u8 = 0b10;
|
||||
|
||||
/// A single u8 that stores whether [BACK] and [FORWARD] are pressed or not
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct BackForwardState(Rc<RefCell<u8>>);
|
||||
|
||||
impl BackForwardState {
|
||||
fn set(&self, button: u8) {
|
||||
*self.0.borrow_mut() |= button
|
||||
}
|
||||
|
||||
fn remove(&self, button: u8) {
|
||||
*self.0.borrow_mut() &= !button
|
||||
}
|
||||
|
||||
fn has(&self, button: u8) -> bool {
|
||||
let state = *self.0.borrow();
|
||||
state & !button != state
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Unix platform extensions for [`WebContext`](super::WebContext).
|
||||
|
||||
use crate::{Error, RequestAsyncResponder};
|
||||
use gtk::glib::{self, MainContext, ObjectExt};
|
||||
use http::{header::CONTENT_TYPE, HeaderName, HeaderValue, Request, Response as HttpResponse};
|
||||
use soup::{MessageHeaders, MessageHeadersType};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
env::current_dir,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
};
|
||||
use webkit2gtk::{
|
||||
ApplicationInfo, AutomationSessionExt, CookiePersistentStorage, DownloadExt, SecurityManagerExt,
|
||||
URIRequest, URIRequestExt, URISchemeRequest, URISchemeRequestExt, URISchemeResponse,
|
||||
URISchemeResponseExt, WebContext, WebContextExt as Webkit2gtkContextExt, WebView, WebViewExt,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebContextImpl {
|
||||
context: WebContext,
|
||||
automation: bool,
|
||||
app_info: Option<ApplicationInfo>,
|
||||
}
|
||||
|
||||
impl WebContextImpl {
|
||||
pub fn new(data_directory: Option<&Path>) -> Self {
|
||||
use webkit2gtk::{CookieManagerExt, WebsiteDataManager, WebsiteDataManagerExt};
|
||||
let mut context_builder = WebContext::builder();
|
||||
if let Some(data_directory) = data_directory {
|
||||
let data_manager = WebsiteDataManager::builder()
|
||||
// TODO: Consider taking a cache_directory so this can be in XDG_CACHE_HOME.
|
||||
.base_cache_directory(data_directory.to_string_lossy())
|
||||
.base_data_directory(data_directory.to_string_lossy())
|
||||
.build();
|
||||
if let Some(cookie_manager) = data_manager.cookie_manager() {
|
||||
cookie_manager.set_persistent_storage(
|
||||
&data_directory.join("cookies").to_string_lossy(),
|
||||
CookiePersistentStorage::Text,
|
||||
);
|
||||
}
|
||||
context_builder = context_builder.website_data_manager(&data_manager);
|
||||
}
|
||||
let context = context_builder.build();
|
||||
|
||||
Self::create_context(context)
|
||||
}
|
||||
|
||||
pub fn new_ephemeral() -> Self {
|
||||
let context = WebContext::new_ephemeral();
|
||||
|
||||
Self::create_context(context)
|
||||
}
|
||||
|
||||
pub fn create_context(context: WebContext) -> Self {
|
||||
let automation = false;
|
||||
context.set_automation_allowed(automation);
|
||||
|
||||
// e.g. wry 0.9.4
|
||||
let app_info = ApplicationInfo::new();
|
||||
app_info.set_name(env!("CARGO_PKG_NAME"));
|
||||
app_info.set_version(
|
||||
env!("CARGO_PKG_VERSION_MAJOR")
|
||||
.parse()
|
||||
.expect("invalid wry version major"),
|
||||
env!("CARGO_PKG_VERSION_MINOR")
|
||||
.parse()
|
||||
.expect("invalid wry version minor"),
|
||||
env!("CARGO_PKG_VERSION_PATCH")
|
||||
.parse()
|
||||
.expect("invalid wry version patch"),
|
||||
);
|
||||
|
||||
Self {
|
||||
context,
|
||||
automation,
|
||||
app_info: Some(app_info),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_allows_automation(&mut self, flag: bool) {
|
||||
self.automation = flag;
|
||||
self.context.set_automation_allowed(flag);
|
||||
}
|
||||
|
||||
pub fn set_web_extensions_directory(&mut self, path: &Path) {
|
||||
self
|
||||
.context
|
||||
.set_web_extensions_directory(&path.to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
/// [`WebContext`](super::WebContext) items that only matter on unix.
|
||||
pub trait WebContextExt {
|
||||
/// The GTK [`WebContext`] of all webviews in the context.
|
||||
fn context(&self) -> &WebContext;
|
||||
|
||||
/// Register a custom protocol to the web context.
|
||||
fn register_uri_scheme<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
|
||||
where
|
||||
F: Fn(crate::WebViewId, Request<Vec<u8>>, RequestAsyncResponder) + 'static;
|
||||
|
||||
/// Loads a URI for a [`WebView`].
|
||||
fn load_uri(&self, webview: WebView, url: String, headers: Option<http::HeaderMap>);
|
||||
|
||||
/// If the context allows automation.
|
||||
///
|
||||
/// **Note:** `libwebkit2gtk` only allows 1 automation context at a time.
|
||||
fn allows_automation(&self) -> bool;
|
||||
|
||||
fn register_automation(&mut self, webview: WebView);
|
||||
|
||||
fn register_download_handler(
|
||||
&mut self,
|
||||
download_started_callback: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool>>,
|
||||
download_completed_callback: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
|
||||
);
|
||||
}
|
||||
|
||||
impl WebContextExt for super::WebContext {
|
||||
fn context(&self) -> &WebContext {
|
||||
&self.os.context
|
||||
}
|
||||
|
||||
fn register_uri_scheme<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
|
||||
where
|
||||
F: Fn(crate::WebViewId, Request<Vec<u8>>, RequestAsyncResponder) + 'static,
|
||||
{
|
||||
self.register_custom_protocol(name.to_owned())?;
|
||||
|
||||
// Enable secure context
|
||||
self
|
||||
.os
|
||||
.context
|
||||
.security_manager()
|
||||
.ok_or(Error::MissingManager)?
|
||||
.register_uri_scheme_as_secure(name);
|
||||
|
||||
self.os.context.register_uri_scheme(name, move |request| {
|
||||
#[cfg(feature = "tracing")]
|
||||
let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty).entered();
|
||||
|
||||
if let Some(uri) = request.uri() {
|
||||
let uri = uri.as_str();
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
span.record("uri", uri);
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut http_request = Request::builder().uri(uri).method("GET");
|
||||
|
||||
// Set request http headers
|
||||
if let Some(headers) = request.http_headers() {
|
||||
if let Some(map) = http_request.headers_mut() {
|
||||
headers.foreach(move |k, v| {
|
||||
if let Ok(name) = HeaderName::from_bytes(k.as_bytes()) {
|
||||
if let Ok(value) = HeaderValue::from_bytes(v.as_bytes()) {
|
||||
map.insert(name, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set request http method
|
||||
if let Some(method) = request.http_method() {
|
||||
http_request = http_request.method(method.as_str());
|
||||
}
|
||||
|
||||
let body;
|
||||
#[cfg(feature = "linux-body")]
|
||||
{
|
||||
use gtk::{gdk::prelude::InputStreamExtManual, gio::Cancellable};
|
||||
|
||||
// Set request http body
|
||||
let cancellable: Option<&Cancellable> = None;
|
||||
body = request
|
||||
.http_body()
|
||||
.map(|s| {
|
||||
const BUFFER_LEN: usize = 1024;
|
||||
let mut result = Vec::new();
|
||||
let mut buffer = vec![0; BUFFER_LEN];
|
||||
while let Ok(count) = s.read(&mut buffer[..], cancellable) {
|
||||
if count == BUFFER_LEN {
|
||||
result.append(&mut buffer);
|
||||
buffer.resize(BUFFER_LEN, 0);
|
||||
} else {
|
||||
buffer.truncate(count);
|
||||
result.append(&mut buffer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
#[cfg(not(feature = "linux-body"))]
|
||||
{
|
||||
body = Vec::new();
|
||||
}
|
||||
|
||||
let http_request = match http_request.body(body) {
|
||||
Ok(req) => req,
|
||||
Err(_) => {
|
||||
request.finish_error(&mut gtk::glib::Error::new(
|
||||
glib::UriError::Failed,
|
||||
"Internal server error: could not create request.",
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let request_ = MainThreadRequest(request.clone());
|
||||
let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
|
||||
Box::new(move |http_response| {
|
||||
MainContext::default().invoke(move || {
|
||||
let buffer = http_response.body();
|
||||
let input = gtk::gio::MemoryInputStream::from_bytes(>k::glib::Bytes::from(buffer));
|
||||
let content_type = http_response
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|h| h.to_str().ok());
|
||||
|
||||
let response = URISchemeResponse::new(&input, buffer.len() as i64);
|
||||
response.set_status(http_response.status().as_u16() as u32, None);
|
||||
if let Some(content_type) = content_type {
|
||||
response.set_content_type(content_type);
|
||||
}
|
||||
|
||||
let headers = MessageHeaders::new(MessageHeadersType::Response);
|
||||
for (name, value) in http_response.headers().into_iter() {
|
||||
headers.append(name.as_str(), value.to_str().unwrap_or(""));
|
||||
}
|
||||
response.set_http_headers(headers);
|
||||
request_.finish_with_response(&response);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
|
||||
|
||||
let webview_id = request
|
||||
.web_view()
|
||||
.and_then(|w| unsafe { w.data::<String>(super::WEBVIEW_ID) })
|
||||
.map(|id| unsafe { id.as_ref().clone() })
|
||||
.unwrap_or_default();
|
||||
|
||||
handler(&webview_id, http_request, RequestAsyncResponder { responder });
|
||||
} else {
|
||||
request.finish_error(&mut glib::Error::new(
|
||||
glib::FileError::Exist,
|
||||
"Could not get uri.",
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_uri(&self, webview: WebView, uri: String, headers: Option<http::HeaderMap>) {
|
||||
if let Some(headers) = headers {
|
||||
let req = URIRequest::builder().uri(&uri).build();
|
||||
|
||||
if let Some(ref mut req_headers) = req.http_headers() {
|
||||
for (header, value) in headers.iter() {
|
||||
req_headers.append(
|
||||
header.to_string().as_str(),
|
||||
value.to_str().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
webview.load_request(&req);
|
||||
} else {
|
||||
webview.load_uri(&uri);
|
||||
}
|
||||
}
|
||||
|
||||
fn allows_automation(&self) -> bool {
|
||||
self.os.automation
|
||||
}
|
||||
|
||||
fn register_automation(&mut self, webview: WebView) {
|
||||
if let (true, Some(app_info)) = (self.os.automation, self.os.app_info.take()) {
|
||||
self.os.context.connect_automation_started(move |_, auto| {
|
||||
let webview = webview.clone();
|
||||
auto.set_application_info(&app_info);
|
||||
|
||||
// We do **NOT** support arbitrarily creating new webviews.
|
||||
// To support this in the future, we would need a way to specify the
|
||||
// default WindowBuilder to use to create the window it will use, and
|
||||
// possibly "default" webview attributes. Difficulty comes in for controlling
|
||||
// the owned Window that would need to be used.
|
||||
//
|
||||
// Instead, we just pass the first created webview.
|
||||
auto.connect_create_web_view(None, move |_| webview.clone());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn register_download_handler(
|
||||
&mut self,
|
||||
download_started_handler: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool>>,
|
||||
download_completed_handler: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
|
||||
) {
|
||||
let context = &self.os.context;
|
||||
|
||||
let download_started_handler = Rc::new(RefCell::new(download_started_handler));
|
||||
let failed = Rc::new(RefCell::new(false));
|
||||
|
||||
context.connect_download_started(move |_context, download| {
|
||||
let download_started_handler = download_started_handler.clone();
|
||||
download.connect_decide_destination(move |download, suggested_filename| {
|
||||
if let Some(uri) = download.request().and_then(|req| req.uri()) {
|
||||
let uri = uri.to_string();
|
||||
|
||||
if let Some(download_started_handler) = download_started_handler.borrow_mut().as_mut() {
|
||||
let mut download_destination =
|
||||
dirs::download_dir().unwrap_or_else(|| current_dir().unwrap_or_default());
|
||||
|
||||
let (mut suggested_filename, ext) = suggested_filename
|
||||
.split_once('.')
|
||||
.map(|(base, ext)| (base, format!(".{ext}")))
|
||||
.unwrap_or((suggested_filename, "".to_string()));
|
||||
|
||||
// For `data:` downloads, webkitgtk will suggest to use the raw data as the filename if the dev provided no name,
|
||||
// for example `"data:attachment/text,sometext"` will result in `text,sometext` but longer data URLs will
|
||||
// result in a cut-off filename, which makes it hard to predict reliably.
|
||||
// TODO: If this keeps causing problems, just remove it and use whatever file name webkitgtk suggests.
|
||||
if uri.starts_with("data:") {
|
||||
if let Some((_, uri_stripped)) = uri.split_once('/') {
|
||||
if let Some((uri_stripped, _)) = uri_stripped.split_once(',') {
|
||||
if suggested_filename.starts_with(&format!("{uri_stripped},")) {
|
||||
suggested_filename = "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
download_destination.push(format!("{suggested_filename}{ext}"));
|
||||
|
||||
// WebView2 does not overwrite files but appends numbers
|
||||
let mut counter = 1;
|
||||
while download_destination.exists() {
|
||||
download_destination.set_file_name(format!("{suggested_filename} ({counter}){ext}"));
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
if download_started_handler(uri, &mut download_destination) {
|
||||
download.set_destination(&download_destination.to_string_lossy());
|
||||
} else {
|
||||
download.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: check if we may also need `false`
|
||||
true
|
||||
});
|
||||
|
||||
download.connect_failed({
|
||||
let failed = failed.clone();
|
||||
move |_, _error| {
|
||||
*failed.borrow_mut() = true;
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(download_completed_handler) = download_completed_handler.clone() {
|
||||
download.connect_finished({
|
||||
let failed = failed.clone();
|
||||
move |download| {
|
||||
if let Some(uri) = download.request().and_then(|req| req.uri()) {
|
||||
let failed = *failed.borrow();
|
||||
let uri = uri.to_string();
|
||||
download_completed_handler(
|
||||
uri,
|
||||
(!failed)
|
||||
.then(|| download.destination().map(PathBuf::from))
|
||||
.flatten(),
|
||||
!failed,
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct MainThreadRequest(URISchemeRequest);
|
||||
|
||||
impl MainThreadRequest {
|
||||
fn finish_with_response(&self, response: &URISchemeResponse) {
|
||||
self.0.finish_with_response(response);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for MainThreadRequest {}
|
||||
unsafe impl Sync for MainThreadRequest {}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// A silly implementation of file drop handling for Windows!
|
||||
|
||||
use crate::DragDropEvent;
|
||||
|
||||
use std::{
|
||||
cell::UnsafeCell,
|
||||
ffi::OsString,
|
||||
os::{raw::c_void, windows::ffi::OsStringExt},
|
||||
path::PathBuf,
|
||||
ptr,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use windows::{
|
||||
core::{implement, BOOL},
|
||||
Win32::{
|
||||
Foundation::{DRAGDROP_E_INVALIDHWND, HWND, LPARAM, POINT, POINTL},
|
||||
Graphics::Gdi::ScreenToClient,
|
||||
System::{
|
||||
Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL},
|
||||
Ole::{
|
||||
IDropTarget, IDropTarget_Impl, RegisterDragDrop, RevokeDragDrop, CF_HDROP, DROPEFFECT,
|
||||
DROPEFFECT_COPY, DROPEFFECT_NONE,
|
||||
},
|
||||
SystemServices::MODIFIERKEYS_FLAGS,
|
||||
},
|
||||
UI::{
|
||||
Shell::{DragFinish, DragQueryFileW, HDROP},
|
||||
WindowsAndMessaging::EnumChildWindows,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DragDropController {
|
||||
drop_targets: Vec<IDropTarget>,
|
||||
}
|
||||
|
||||
impl DragDropController {
|
||||
#[inline]
|
||||
pub(crate) fn new(hwnd: HWND, handler: Box<dyn Fn(DragDropEvent) -> bool>) -> Self {
|
||||
let mut controller = DragDropController::default();
|
||||
|
||||
let handler = Rc::new(handler);
|
||||
|
||||
// Enumerate child windows to find the WebView2 "window" and override!
|
||||
{
|
||||
let mut callback = |hwnd| controller.inject_in_hwnd(hwnd, handler.clone());
|
||||
let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback;
|
||||
let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) };
|
||||
let lparam = LPARAM(closure_pointer_pointer as _);
|
||||
unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let closure = &mut *(lparam.0 as *mut c_void as *mut &mut dyn FnMut(HWND) -> bool);
|
||||
closure(hwnd).into()
|
||||
}
|
||||
let _ = unsafe { EnumChildWindows(Some(hwnd), Some(enumerate_callback), lparam) };
|
||||
}
|
||||
|
||||
controller
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn inject_in_hwnd(&mut self, hwnd: HWND, handler: Rc<dyn Fn(DragDropEvent) -> bool>) -> bool {
|
||||
let drag_drop_target: IDropTarget = DragDropTarget::new(hwnd, handler).into();
|
||||
if unsafe { RevokeDragDrop(hwnd) } != Err(DRAGDROP_E_INVALIDHWND.into())
|
||||
&& unsafe { RegisterDragDrop(hwnd, &drag_drop_target) }.is_ok()
|
||||
{
|
||||
self.drop_targets.push(drag_drop_target);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[implement(IDropTarget)]
|
||||
pub struct DragDropTarget {
|
||||
hwnd: HWND,
|
||||
listener: Rc<dyn Fn(DragDropEvent) -> bool>,
|
||||
cursor_effect: UnsafeCell<DROPEFFECT>,
|
||||
enter_is_valid: UnsafeCell<bool>, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */
|
||||
}
|
||||
|
||||
impl DragDropTarget {
|
||||
pub fn new(hwnd: HWND, listener: Rc<dyn Fn(DragDropEvent) -> bool>) -> DragDropTarget {
|
||||
Self {
|
||||
hwnd,
|
||||
listener,
|
||||
cursor_effect: DROPEFFECT_NONE.into(),
|
||||
enter_is_valid: false.into(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn iterate_filenames<F>(
|
||||
data_obj: windows_core::Ref<'_, IDataObject>,
|
||||
mut callback: F,
|
||||
) -> Option<HDROP>
|
||||
where
|
||||
F: FnMut(PathBuf),
|
||||
{
|
||||
let drop_format = FORMATETC {
|
||||
cfFormat: CF_HDROP.0,
|
||||
ptd: ptr::null_mut(),
|
||||
dwAspect: DVASPECT_CONTENT.0,
|
||||
lindex: -1,
|
||||
tymed: TYMED_HGLOBAL.0 as u32,
|
||||
};
|
||||
|
||||
match data_obj
|
||||
.as_ref()
|
||||
.expect("Received null IDataObject")
|
||||
.GetData(&drop_format)
|
||||
{
|
||||
Ok(medium) => {
|
||||
let hdrop = HDROP(medium.u.hGlobal.0 as _);
|
||||
|
||||
// The second parameter (0xFFFFFFFF) instructs the function to return the item count
|
||||
let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, None);
|
||||
|
||||
for i in 0..item_count {
|
||||
// Get the length of the path string NOT including the terminating null character.
|
||||
// Previously, this was using a fixed size array of MAX_PATH length, but the
|
||||
// Windows API allows longer paths under certain circumstances.
|
||||
let character_count = DragQueryFileW(hdrop, i, None) as usize;
|
||||
|
||||
// Fill path_buf with the null-terminated file name
|
||||
let str_len = character_count + 1;
|
||||
let mut path_buf = vec![0; str_len];
|
||||
DragQueryFileW(hdrop, i, Some(&mut path_buf));
|
||||
callback(OsString::from_wide(&path_buf[0..character_count]).into());
|
||||
}
|
||||
|
||||
Some(hdrop)
|
||||
}
|
||||
Err(_error) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!(
|
||||
"{}",
|
||||
match _error.code() {
|
||||
windows::Win32::Foundation::DV_E_FORMATETC => {
|
||||
// If the dropped item is not a file this error will occur.
|
||||
// In this case it is OK to return without taking further action.
|
||||
"Error occurred while processing dropped/hovered item: item is not a file."
|
||||
}
|
||||
_ => "Unexpected error occurred while processing dropped/hovered item.",
|
||||
}
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
impl IDropTarget_Impl for DragDropTarget_Impl {
|
||||
fn DragEnter(
|
||||
&self,
|
||||
pDataObj: windows_core::Ref<'_, IDataObject>,
|
||||
_grfKeyState: MODIFIERKEYS_FLAGS,
|
||||
pt: &POINTL,
|
||||
pdwEffect: *mut DROPEFFECT,
|
||||
) -> windows::core::Result<()> {
|
||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
||||
let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) };
|
||||
|
||||
let mut paths = Vec::new();
|
||||
let hdrop = unsafe { DragDropTarget::iterate_filenames(pDataObj, |path| paths.push(path)) };
|
||||
|
||||
let enter_is_valid = hdrop.is_some();
|
||||
|
||||
if !enter_is_valid {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
unsafe {
|
||||
*self.enter_is_valid.get() = enter_is_valid;
|
||||
}
|
||||
|
||||
(self.listener)(DragDropEvent::Enter {
|
||||
paths,
|
||||
position: (pt.x as _, pt.y as _),
|
||||
});
|
||||
|
||||
let cursor_effect = if enter_is_valid {
|
||||
DROPEFFECT_COPY
|
||||
} else {
|
||||
DROPEFFECT_NONE
|
||||
};
|
||||
|
||||
unsafe {
|
||||
*pdwEffect = cursor_effect;
|
||||
*self.cursor_effect.get() = cursor_effect;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn DragOver(
|
||||
&self,
|
||||
_grfKeyState: MODIFIERKEYS_FLAGS,
|
||||
pt: &POINTL,
|
||||
pdwEffect: *mut DROPEFFECT,
|
||||
) -> windows::core::Result<()> {
|
||||
if unsafe { *self.enter_is_valid.get() } {
|
||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
||||
let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) };
|
||||
(self.listener)(DragDropEvent::Over {
|
||||
position: (pt.x as _, pt.y as _),
|
||||
});
|
||||
}
|
||||
|
||||
unsafe { *pdwEffect = *self.cursor_effect.get() };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn DragLeave(&self) -> windows::core::Result<()> {
|
||||
if unsafe { *self.enter_is_valid.get() } {
|
||||
(self.listener)(DragDropEvent::Leave);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn Drop(
|
||||
&self,
|
||||
pDataObj: windows_core::Ref<'_, IDataObject>,
|
||||
_grfKeyState: MODIFIERKEYS_FLAGS,
|
||||
pt: &POINTL,
|
||||
_pdwEffect: *mut DROPEFFECT,
|
||||
) -> windows::core::Result<()> {
|
||||
if unsafe { *self.enter_is_valid.get() } {
|
||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
||||
let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) };
|
||||
|
||||
let mut paths = Vec::new();
|
||||
let hdrop = unsafe { DragDropTarget::iterate_filenames(pDataObj, |path| paths.push(path)) };
|
||||
(self.listener)(DragDropEvent::Drop {
|
||||
paths,
|
||||
position: (pt.x as _, pt.y as _),
|
||||
});
|
||||
|
||||
if let Some(hdrop) = hdrop {
|
||||
unsafe { DragFinish(hdrop) };
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use windows::{
|
||||
core::{HRESULT, HSTRING, PCSTR},
|
||||
Win32::{
|
||||
Foundation::{FARPROC, HWND, S_OK},
|
||||
Graphics::Gdi::{
|
||||
GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST,
|
||||
},
|
||||
System::LibraryLoader::{GetProcAddress, LoadLibraryW},
|
||||
UI::{
|
||||
HiDpi::{MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE},
|
||||
WindowsAndMessaging::IsProcessDPIAware,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fn get_function_impl(library: &str, function: &str) -> FARPROC {
|
||||
let library = HSTRING::from(library);
|
||||
assert_eq!(function.chars().last(), Some('\0'));
|
||||
|
||||
// Library names we will use are ASCII so we can use the A version to avoid string conversion.
|
||||
let module = unsafe { LoadLibraryW(&library) }.unwrap_or_default();
|
||||
if module.is_invalid() {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) }
|
||||
}
|
||||
|
||||
macro_rules! get_function {
|
||||
($lib:expr, $func:ident) => {
|
||||
crate::webview2::util::get_function_impl($lib, concat!(stringify!($func), '\0'))
|
||||
.map(|f| unsafe { std::mem::transmute::<_, $func>(f) })
|
||||
};
|
||||
}
|
||||
|
||||
pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32;
|
||||
pub type GetDpiForMonitor = unsafe extern "system" fn(
|
||||
hmonitor: HMONITOR,
|
||||
dpi_type: MONITOR_DPI_TYPE,
|
||||
dpi_x: *mut u32,
|
||||
dpi_y: *mut u32,
|
||||
) -> HRESULT;
|
||||
|
||||
static GET_DPI_FOR_WINDOW: Lazy<Option<GetDpiForWindow>> =
|
||||
Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));
|
||||
static GET_DPI_FOR_MONITOR: Lazy<Option<GetDpiForMonitor>> =
|
||||
Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor));
|
||||
|
||||
pub const BASE_DPI: u32 = 96;
|
||||
pub fn dpi_to_scale_factor(dpi: u32) -> f64 {
|
||||
dpi as f64 / BASE_DPI as f64
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 {
|
||||
if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW {
|
||||
// We are on Windows 10 Anniversary Update (1607) or later.
|
||||
match GetDpiForWindow(hwnd) {
|
||||
0 => BASE_DPI, // 0 is returned if hwnd is invalid
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
dpi => dpi as u32,
|
||||
}
|
||||
} else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR {
|
||||
// We are on Windows 8.1 or later.
|
||||
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
if monitor.is_invalid() {
|
||||
return BASE_DPI;
|
||||
}
|
||||
|
||||
let mut dpi_x = 0;
|
||||
let mut dpi_y = 0;
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK {
|
||||
dpi_x as u32
|
||||
} else {
|
||||
BASE_DPI
|
||||
}
|
||||
} else {
|
||||
let hdc = GetDC(Some(hwnd));
|
||||
if hdc.is_invalid() {
|
||||
return BASE_DPI;
|
||||
}
|
||||
|
||||
// We are on Vista or later.
|
||||
if IsProcessDPIAware().as_bool() {
|
||||
// If the process is DPI aware, then scaling must be handled by the application using
|
||||
// this DPI value.
|
||||
GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32
|
||||
} else {
|
||||
// If the process is DPI unaware, then scaling is performed by the OS; we thus return
|
||||
// 96 (scale factor 1.0) to prevent the window from being re-scaled by both the
|
||||
// application and the WM.
|
||||
BASE_DPI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{ffi::c_void, ptr::null_mut};
|
||||
|
||||
use objc2::{
|
||||
define_class, msg_send,
|
||||
rc::Retained,
|
||||
runtime::{AnyObject, NSObject},
|
||||
AllocAnyThread, DefinedClass,
|
||||
};
|
||||
use objc2_foundation::{
|
||||
ns_string, NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions,
|
||||
NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSString,
|
||||
};
|
||||
|
||||
use crate::WryWebView;
|
||||
pub struct DocumentTitleChangedObserverIvars {
|
||||
pub object: Retained<WryWebView>,
|
||||
pub handler: Box<dyn Fn(String)>,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSObject))]
|
||||
#[ivars = DocumentTitleChangedObserverIvars]
|
||||
pub struct DocumentTitleChangedObserver;
|
||||
|
||||
/// NSKeyValueObserving.
|
||||
impl DocumentTitleChangedObserver {
|
||||
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
|
||||
fn observe_value_for_key_path(
|
||||
&self,
|
||||
key_path: Option<&NSString>,
|
||||
of_object: Option<&AnyObject>,
|
||||
_change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
|
||||
_context: *mut c_void,
|
||||
) {
|
||||
if let (Some(key_path), Some(object)) = (key_path, of_object) {
|
||||
unsafe {
|
||||
if key_path.isEqualToString(ns_string!("title")) {
|
||||
let handler = &self.ivars().handler;
|
||||
// if !handler.is_null() {
|
||||
let title: *const NSString = msg_send![object, title];
|
||||
handler((*title).to_string());
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl NSObjectProtocol for DocumentTitleChangedObserver {}
|
||||
);
|
||||
|
||||
impl DocumentTitleChangedObserver {
|
||||
pub fn new(webview: Retained<WryWebView>, handler: Box<dyn Fn(String)>) -> Retained<Self> {
|
||||
let observer = Self::alloc().set_ivars(DocumentTitleChangedObserverIvars {
|
||||
object: webview,
|
||||
handler,
|
||||
});
|
||||
|
||||
let observer: Retained<Self> = unsafe { msg_send![super(observer), init] };
|
||||
|
||||
unsafe {
|
||||
observer
|
||||
.ivars()
|
||||
.object
|
||||
.addObserver_forKeyPath_options_context(
|
||||
&observer,
|
||||
ns_string!("title"),
|
||||
NSKeyValueObservingOptions::New,
|
||||
null_mut(),
|
||||
);
|
||||
}
|
||||
|
||||
observer
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DocumentTitleChangedObserver {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self
|
||||
.ivars()
|
||||
.object
|
||||
.removeObserver_forKeyPath(self, ns_string!("title"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pub mod document_title_changed_observer;
|
||||
pub mod url_scheme_handler;
|
||||
pub mod wry_download_delegate;
|
||||
pub mod wry_navigation_delegate;
|
||||
pub mod wry_web_view;
|
||||
pub mod wry_web_view_delegate;
|
||||
pub mod wry_web_view_parent;
|
||||
pub mod wry_web_view_ui_delegate;
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::{c_char, c_void, CStr},
|
||||
panic::AssertUnwindSafe,
|
||||
ptr::NonNull,
|
||||
};
|
||||
|
||||
use http::{
|
||||
header::{CONTENT_LENGTH, CONTENT_TYPE},
|
||||
Request, Response as HttpResponse, StatusCode, Version,
|
||||
};
|
||||
use objc2::{
|
||||
rc::Retained,
|
||||
runtime::{AnyClass, AnyObject, ClassBuilder, ProtocolObject},
|
||||
AllocAnyThread, ClassType, Message,
|
||||
};
|
||||
use objc2_foundation::{
|
||||
NSData, NSHTTPURLResponse, NSMutableDictionary, NSObject, NSObjectProtocol, NSString, NSURL,
|
||||
NSUUID,
|
||||
};
|
||||
use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask};
|
||||
|
||||
use crate::{wkwebview::WEBVIEW_STATE, RequestAsyncResponder, WryWebView};
|
||||
|
||||
pub fn create(name: &str) -> &AnyClass {
|
||||
unsafe {
|
||||
// Include the address of WEBVIEW_STATE in the class name so that each dylib in the process
|
||||
// gets its own ObjC class with method pointers into its own code and data segments.
|
||||
let unique_id = std::ptr::addr_of!(WEBVIEW_STATE) as usize;
|
||||
let scheme_name = format!("{name}URLSchemeHandler_{unique_id:x}\0");
|
||||
let scheme_name = CStr::from_bytes_with_nul(scheme_name.as_bytes()).unwrap();
|
||||
let cls = ClassBuilder::new(scheme_name, NSObject::class());
|
||||
match cls {
|
||||
Some(mut cls) => {
|
||||
cls.add_ivar::<*mut c_char>(c"webview_id");
|
||||
cls.add_ivar::<usize>(c"protocol_index");
|
||||
cls.add_method(
|
||||
objc2::sel!(webView:startURLSchemeTask:),
|
||||
start_task as extern "C" fn(_, _, _, _),
|
||||
);
|
||||
cls.add_method(
|
||||
objc2::sel!(webView:stopURLSchemeTask:),
|
||||
stop_task as extern "C" fn(_, _, _, _),
|
||||
);
|
||||
cls.register()
|
||||
}
|
||||
None => AnyClass::get(scheme_name).expect("Failed to get the class definition"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task handler for custom protocol
|
||||
extern "C" fn start_task(
|
||||
this: &AnyObject,
|
||||
_sel: objc2::runtime::Sel,
|
||||
webview: &WryWebView,
|
||||
task: &ProtocolObject<dyn WKURLSchemeTask>,
|
||||
) {
|
||||
unsafe {
|
||||
#[cfg(feature = "tracing")]
|
||||
let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty)
|
||||
.entered();
|
||||
|
||||
let task_key = task.hash(); // hash by task object address
|
||||
let task_uuid = webview.add_custom_task_key(task_key);
|
||||
|
||||
let ivar = this.class().instance_variable(c"webview_id").unwrap();
|
||||
let webview_id_ptr: *mut c_char = *ivar.load(this);
|
||||
let webview_id = CStr::from_ptr(webview_id_ptr)
|
||||
.to_str()
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let ivar = this.class().instance_variable(c"protocol_index").unwrap();
|
||||
let protocol_index: usize = *ivar.load(this);
|
||||
|
||||
let function = WEBVIEW_STATE
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(webview_id)
|
||||
.and_then(|v| v.protocol_ptrs.get(protocol_index))
|
||||
.cloned();
|
||||
|
||||
if let Some(function) = function {
|
||||
// Get url request
|
||||
let request = task.request();
|
||||
let url = request.URL().unwrap();
|
||||
|
||||
let uri = url.absoluteString().unwrap().to_string();
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
span.record("uri", uri.clone());
|
||||
|
||||
// Get request method (GET, POST, PUT etc...)
|
||||
let method = request.HTTPMethod().unwrap().to_string();
|
||||
|
||||
// Prepare our HttpRequest
|
||||
let mut http_request = Request::builder().uri(uri).method(method.as_str());
|
||||
|
||||
// Get body
|
||||
let mut sent_form_body = Vec::new();
|
||||
let body = request.HTTPBody();
|
||||
let body_stream = request.HTTPBodyStream();
|
||||
if let Some(body) = body {
|
||||
sent_form_body = body.to_vec();
|
||||
} else if let Some(body_stream) = body_stream {
|
||||
body_stream.open();
|
||||
|
||||
while body_stream.hasBytesAvailable() {
|
||||
sent_form_body.reserve(128);
|
||||
let p = sent_form_body.as_mut_ptr().add(sent_form_body.len());
|
||||
let read_length = sent_form_body.capacity() - sent_form_body.len();
|
||||
let count = body_stream.read_maxLength(NonNull::new(p).unwrap(), read_length);
|
||||
sent_form_body.set_len(sent_form_body.len() + count as usize);
|
||||
}
|
||||
|
||||
body_stream.close();
|
||||
}
|
||||
|
||||
// Extract all headers fields
|
||||
let all_headers = request.allHTTPHeaderFields();
|
||||
|
||||
// get all our headers values and inject them in our request
|
||||
if let Some(all_headers) = all_headers {
|
||||
for current_header in all_headers.allKeys().iter() {
|
||||
let header_value = all_headers.valueForKey(¤t_header).unwrap();
|
||||
// inject the header into the request
|
||||
http_request = http_request.header(current_header.to_string(), header_value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let respond_with_404 = || {
|
||||
let urlresponse = NSHTTPURLResponse::alloc();
|
||||
let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields(
|
||||
urlresponse,
|
||||
&url,
|
||||
StatusCode::NOT_FOUND.as_u16().try_into().unwrap(),
|
||||
Some(&NSString::from_str(
|
||||
format!("{:#?}", Version::HTTP_11).as_str(),
|
||||
)),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
task.didReceiveResponse(&response);
|
||||
// Finish
|
||||
task.didFinish();
|
||||
};
|
||||
|
||||
fn check_webview_id_valid(webview_id: &str) -> crate::Result<()> {
|
||||
if !WEBVIEW_STATE.read().unwrap().contains_key(webview_id) {
|
||||
return Err(crate::Error::CustomProtocolTaskInvalid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Task may not live longer than async custom protocol handler.
|
||||
///
|
||||
/// There are roughly 2 ways to cause segfault:
|
||||
/// 1. Task has stopped. pointer of the task not valid anymore.
|
||||
/// 2. Task had stopped, but the pointer of the task has allocated to a new task.
|
||||
/// Outdated custom handler may call to the new task instance and cause segfault.
|
||||
fn check_task_is_valid(
|
||||
webview: &WryWebView,
|
||||
task_key: usize,
|
||||
current_uuid: Retained<NSUUID>,
|
||||
) -> crate::Result<()> {
|
||||
let latest_task_uuid = webview.get_custom_task_uuid(task_key);
|
||||
let Some(latest_uuid) = latest_task_uuid else {
|
||||
return Err(crate::Error::CustomProtocolTaskInvalid);
|
||||
};
|
||||
if latest_uuid != current_uuid {
|
||||
return Err(crate::Error::CustomProtocolTaskInvalid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// send response
|
||||
match http_request.body(sent_form_body) {
|
||||
Ok(final_request) => {
|
||||
let webview = webview.retain();
|
||||
let task = task.retain();
|
||||
let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
|
||||
Box::new(move |sent_response| {
|
||||
// Consolidate checks before calling into `did*` methods.
|
||||
let validate = || -> crate::Result<()> {
|
||||
check_webview_id_valid(webview_id)?;
|
||||
check_task_is_valid(&webview, task_key, task_uuid.clone())?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// Perform an upfront validation
|
||||
if let Err(_e) = validate() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("Task invalid before sending response: {:?}", _e);
|
||||
return; // If invalid, return early without calling task methods.
|
||||
}
|
||||
|
||||
unsafe fn response(
|
||||
// FIXME: though we give it a static lifetime, it's not guaranteed to be valid.
|
||||
task: Retained<ProtocolObject<dyn WKURLSchemeTask>>,
|
||||
// FIXME: though we give it a static lifetime, it's not guaranteed to be valid.
|
||||
webview: Retained<WryWebView>,
|
||||
task_key: usize,
|
||||
task_uuid: Retained<NSUUID>,
|
||||
webview_id: &str,
|
||||
url: Retained<NSURL>,
|
||||
sent_response: HttpResponse<Cow<'_, [u8]>>,
|
||||
) -> crate::Result<()> {
|
||||
// Validate
|
||||
check_webview_id_valid(webview_id)?;
|
||||
check_task_is_valid(&webview, task_key, task_uuid.clone())?;
|
||||
|
||||
let content = sent_response.body();
|
||||
// default: application/octet-stream, but should be provided by the client
|
||||
let wanted_mime = sent_response.headers().get(CONTENT_TYPE);
|
||||
// default to 200
|
||||
let wanted_status_code = sent_response.status().as_u16() as i32;
|
||||
// default to HTTP/1.1
|
||||
let wanted_version = format!("{:#?}", sent_response.version());
|
||||
|
||||
let headers = NSMutableDictionary::new();
|
||||
if let Some(mime) = wanted_mime {
|
||||
headers.insert(
|
||||
&*NSString::from_str(CONTENT_TYPE.as_str()),
|
||||
&*NSString::from_str(mime.to_str().unwrap()),
|
||||
);
|
||||
}
|
||||
headers.insert(
|
||||
&*NSString::from_str(CONTENT_LENGTH.as_str()),
|
||||
&*NSString::from_str(&content.len().to_string()),
|
||||
);
|
||||
|
||||
// add headers
|
||||
for (name, value) in sent_response.headers().iter() {
|
||||
if let Ok(value) = value.to_str() {
|
||||
headers.insert(
|
||||
&*NSString::from_str(name.as_str()),
|
||||
&*NSString::from_str(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let urlresponse = NSHTTPURLResponse::alloc();
|
||||
let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields(
|
||||
urlresponse,
|
||||
&url,
|
||||
wanted_status_code.try_into().unwrap(),
|
||||
Some(&NSString::from_str(&wanted_version)),
|
||||
Some(&headers),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Re-validate before calling didReceiveResponse
|
||||
check_webview_id_valid(webview_id)?;
|
||||
check_task_is_valid(&webview, task_key, task_uuid.clone())?;
|
||||
|
||||
// Use map_err to convert Option<Retained<Exception>> to crate::Error
|
||||
objc2::exception::catch(AssertUnwindSafe(|| {
|
||||
task.didReceiveResponse(&response);
|
||||
}))
|
||||
.map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?;
|
||||
|
||||
// Send data
|
||||
let data = NSData::alloc();
|
||||
// MIGRATE NOTE: we copied the content to the NSData because content will be freed
|
||||
// when out of scope but NSData will also free the content when it's done and cause doube free.
|
||||
let data = NSData::initWithBytes_length(
|
||||
data,
|
||||
content.as_ptr() as *mut c_void,
|
||||
content.len(),
|
||||
);
|
||||
|
||||
// Check validity again
|
||||
check_webview_id_valid(webview_id)?;
|
||||
check_task_is_valid(&webview, task_key, task_uuid.clone())?;
|
||||
|
||||
objc2::exception::catch(AssertUnwindSafe(|| {
|
||||
task.didReceiveData(&data);
|
||||
}))
|
||||
.map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?;
|
||||
|
||||
check_webview_id_valid(webview_id)?;
|
||||
check_task_is_valid(&webview, task_key, task_uuid)?;
|
||||
|
||||
objc2::exception::catch(AssertUnwindSafe(|| {
|
||||
task.didFinish();
|
||||
}))
|
||||
.map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?;
|
||||
|
||||
if WEBVIEW_STATE.read().unwrap().contains_key(webview_id) {
|
||||
webview.remove_custom_task_key(task_key);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::Error::CustomProtocolTaskInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
|
||||
|
||||
if let Err(_e) = response(
|
||||
task,
|
||||
webview,
|
||||
task_key,
|
||||
task_uuid,
|
||||
webview_id,
|
||||
url,
|
||||
sent_response,
|
||||
) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("Error responding to task: {:?}", _e);
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
|
||||
|
||||
function(
|
||||
webview_id,
|
||||
final_request,
|
||||
RequestAsyncResponder { responder },
|
||||
);
|
||||
}
|
||||
Err(_) => respond_with_404(),
|
||||
};
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!(
|
||||
"Either WebView or WebContext instance is dropped! This handler shouldn't be called."
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn stop_task(
|
||||
_this: &ProtocolObject<dyn WKURLSchemeHandler>,
|
||||
_sel: objc2::runtime::Sel,
|
||||
webview: &WryWebView,
|
||||
task: &ProtocolObject<dyn WKURLSchemeTask>,
|
||||
) {
|
||||
webview.remove_custom_task_key(task.hash());
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
||||
|
||||
use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly};
|
||||
use objc2_foundation::{
|
||||
MainThreadMarker, NSData, NSError, NSObjectProtocol, NSString, NSURLResponse, NSURL,
|
||||
};
|
||||
use objc2_web_kit::{WKDownload, WKDownloadDelegate};
|
||||
|
||||
use crate::wkwebview::download::{download_did_fail, download_did_finish, download_policy};
|
||||
|
||||
pub struct WryDownloadDelegateIvars {
|
||||
pub started: Option<RefCell<Box<dyn FnMut(String, &mut PathBuf) -> bool + 'static>>>,
|
||||
pub completed: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSObject))]
|
||||
#[thread_kind = MainThreadOnly]
|
||||
#[ivars = WryDownloadDelegateIvars]
|
||||
pub struct WryDownloadDelegate;
|
||||
|
||||
unsafe impl NSObjectProtocol for WryDownloadDelegate {}
|
||||
|
||||
unsafe impl WKDownloadDelegate for WryDownloadDelegate {
|
||||
#[unsafe(method(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:))]
|
||||
fn download_policy(
|
||||
&self,
|
||||
download: &WKDownload,
|
||||
response: &NSURLResponse,
|
||||
suggested_filename: &NSString,
|
||||
handler: &block2::Block<dyn Fn(*const NSURL)>,
|
||||
) {
|
||||
download_policy(self, download, response, suggested_filename, handler);
|
||||
}
|
||||
|
||||
#[unsafe(method(downloadDidFinish:))]
|
||||
fn download_did_finish(&self, download: &WKDownload) {
|
||||
download_did_finish(self, download);
|
||||
}
|
||||
|
||||
#[unsafe(method(download:didFailWithError:resumeData:))]
|
||||
fn download_did_fail(&self, download: &WKDownload, error: &NSError, resume_data: &NSData) {
|
||||
download_did_fail(self, download, error, resume_data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WryDownloadDelegate {
|
||||
pub fn new(
|
||||
download_started_handler: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool + 'static>>,
|
||||
download_completed_handler: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
|
||||
mtm: MainThreadMarker,
|
||||
) -> Retained<Self> {
|
||||
let delegate = mtm
|
||||
.alloc::<WryDownloadDelegate>()
|
||||
.set_ivars(WryDownloadDelegateIvars {
|
||||
started: download_started_handler.map(|handler| RefCell::new(handler)),
|
||||
completed: download_completed_handler,
|
||||
});
|
||||
|
||||
unsafe { msg_send![super(delegate), init] }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly};
|
||||
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
|
||||
use objc2_web_kit::{
|
||||
WKDownload, WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate,
|
||||
WKNavigationResponse, WKNavigationResponsePolicy,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
use crate::wkwebview::ios::WKWebView::WKWebView;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_web_kit::WKWebView;
|
||||
|
||||
use crate::{
|
||||
url_from_webview,
|
||||
wkwebview::{
|
||||
download::{navigation_download_action, navigation_download_response},
|
||||
navigation::{
|
||||
did_commit_navigation, did_finish_navigation, navigation_policy, navigation_policy_response,
|
||||
web_content_process_did_terminate,
|
||||
},
|
||||
},
|
||||
PageLoadEvent, WryWebView,
|
||||
};
|
||||
|
||||
use super::wry_download_delegate::WryDownloadDelegate;
|
||||
|
||||
pub struct WryNavigationDelegateIvars {
|
||||
pub pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
|
||||
pub has_download_handler: bool,
|
||||
pub navigation_policy_function: Box<dyn Fn(String) -> bool>,
|
||||
pub download_delegate: Option<Retained<WryDownloadDelegate>>,
|
||||
pub on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent)>>,
|
||||
pub on_web_content_process_terminate_handler: Option<Box<dyn Fn()>>,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSObject))]
|
||||
#[thread_kind = MainThreadOnly]
|
||||
#[ivars = WryNavigationDelegateIvars]
|
||||
pub struct WryNavigationDelegate;
|
||||
|
||||
unsafe impl NSObjectProtocol for WryNavigationDelegate {}
|
||||
|
||||
unsafe impl WKNavigationDelegate for WryNavigationDelegate {
|
||||
#[unsafe(method(webView:decidePolicyForNavigationAction:decisionHandler:))]
|
||||
fn navigation_policy(
|
||||
&self,
|
||||
webview: &WKWebView,
|
||||
action: &WKNavigationAction,
|
||||
handler: &block2::Block<dyn Fn(WKNavigationActionPolicy)>,
|
||||
) {
|
||||
navigation_policy(self, webview, action, handler);
|
||||
}
|
||||
|
||||
#[unsafe(method(webView:decidePolicyForNavigationResponse:decisionHandler:))]
|
||||
fn navigation_policy_response(
|
||||
&self,
|
||||
webview: &WKWebView,
|
||||
response: &WKNavigationResponse,
|
||||
handler: &block2::Block<dyn Fn(WKNavigationResponsePolicy)>,
|
||||
) {
|
||||
navigation_policy_response(self, webview, response, handler);
|
||||
}
|
||||
|
||||
#[unsafe(method(webView:didFinishNavigation:))]
|
||||
fn did_finish_navigation(&self, webview: &WKWebView, navigation: &WKNavigation) {
|
||||
did_finish_navigation(self, webview, navigation);
|
||||
}
|
||||
|
||||
#[unsafe(method(webView:didCommitNavigation:))]
|
||||
fn did_commit_navigation(&self, webview: &WKWebView, navigation: &WKNavigation) {
|
||||
did_commit_navigation(self, webview, navigation);
|
||||
}
|
||||
|
||||
#[unsafe(method(webView:navigationAction:didBecomeDownload:))]
|
||||
fn navigation_download_action(
|
||||
&self,
|
||||
webview: &WKWebView,
|
||||
action: &WKNavigationAction,
|
||||
download: &WKDownload,
|
||||
) {
|
||||
navigation_download_action(self, webview, action, download);
|
||||
}
|
||||
|
||||
#[unsafe(method(webView:navigationResponse:didBecomeDownload:))]
|
||||
fn navigation_download_response(
|
||||
&self,
|
||||
webview: &WKWebView,
|
||||
response: &WKNavigationResponse,
|
||||
download: &WKDownload,
|
||||
) {
|
||||
navigation_download_response(self, webview, response, download);
|
||||
}
|
||||
|
||||
#[unsafe(method(webViewWebContentProcessDidTerminate:))]
|
||||
fn web_content_process_did_terminate(&self, webview: &WKWebView) {
|
||||
web_content_process_did_terminate(self, webview);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WryNavigationDelegate {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
webview: Retained<WryWebView>,
|
||||
pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
|
||||
has_download_handler: bool,
|
||||
navigation_handler: Option<Box<dyn Fn(String) -> bool>>,
|
||||
download_delegate: Option<Retained<WryDownloadDelegate>>,
|
||||
on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent, String)>>,
|
||||
on_web_content_process_terminate_handler: Option<Box<dyn Fn()>>,
|
||||
mtm: MainThreadMarker,
|
||||
) -> Retained<Self> {
|
||||
let navigation_policy_function = Box::new(move |url: String| -> bool {
|
||||
navigation_handler
|
||||
.as_ref()
|
||||
.map_or(true, |navigation_handler| (navigation_handler)(url))
|
||||
});
|
||||
|
||||
let on_page_load_handler = if let Some(handler) = on_page_load_handler {
|
||||
let custom_handler = Box::new(move |event| {
|
||||
handler(event, url_from_webview(&webview).unwrap_or_default());
|
||||
}) as Box<dyn Fn(PageLoadEvent)>;
|
||||
Some(custom_handler)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let on_web_content_process_terminate_handler =
|
||||
if let Some(handler) = on_web_content_process_terminate_handler {
|
||||
let custom_handler = Box::new(move || {
|
||||
handler();
|
||||
}) as Box<dyn Fn()>;
|
||||
Some(custom_handler)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let delegate = mtm
|
||||
.alloc::<WryNavigationDelegate>()
|
||||
.set_ivars(WryNavigationDelegateIvars {
|
||||
pending_scripts,
|
||||
navigation_policy_function,
|
||||
has_download_handler,
|
||||
download_delegate,
|
||||
on_page_load_handler,
|
||||
on_web_content_process_terminate_handler,
|
||||
});
|
||||
|
||||
unsafe { msg_send![super(delegate), init] }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2::{define_class, rc::Retained, runtime::Bool, DeclaredClass};
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_app_kit::{NSDraggingDestination, NSEvent};
|
||||
use objc2_foundation::{NSObjectProtocol, NSUUID};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
use crate::wkwebview::ios::WKWebView::WKWebView;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::{
|
||||
wkwebview::{drag_drop, synthetic_mouse_events},
|
||||
DragDropEvent,
|
||||
};
|
||||
#[cfg(target_os = "ios")]
|
||||
use objc2_ui_kit::UIEvent as NSEvent;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_web_kit::WKWebView;
|
||||
|
||||
pub struct WryWebViewIvars {
|
||||
pub(crate) is_child: bool,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) drag_drop_handler: Box<dyn Fn(DragDropEvent) -> bool>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) accept_first_mouse: objc2::runtime::Bool,
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) input_accessory_view_builder: Option<Box<crate::InputAccessoryViewBuilder>>,
|
||||
pub(crate) custom_protocol_task_ids: Mutex<HashMap<usize, Retained<NSUUID>>>,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(WKWebView))]
|
||||
#[ivars = WryWebViewIvars]
|
||||
pub struct WryWebView;
|
||||
|
||||
/// Overridden NSView methods.
|
||||
impl WryWebView {
|
||||
#[unsafe(method(performKeyEquivalent:))]
|
||||
fn perform_key_equivalent(&self, event: &NSEvent) -> Bool {
|
||||
// This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426
|
||||
// FIXME: When the webview is a child webview, performKeyEquivalent always return YES
|
||||
// and stop propagating the event to the window, hence the menu shortcut won't be
|
||||
// triggered. However, overriding this method also means the cmd+key event won't be
|
||||
// handled in webview, which means the key cannot be listened by JavaScript.
|
||||
if self.ivars().is_child {
|
||||
Bool::NO
|
||||
} else {
|
||||
unsafe { objc2::msg_send![super(self), performKeyEquivalent: event] }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method(acceptsFirstMouse:))]
|
||||
fn accept_first_mouse(&self, _event: &NSEvent) -> Bool {
|
||||
self.ivars().accept_first_mouse
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
#[unsafe(method_id(inputAccessoryView))]
|
||||
fn input_accessory_view(&self) -> Option<Retained<objc2_ui_kit::UIView>> {
|
||||
if let Some(builder) = &self.ivars().input_accessory_view_builder {
|
||||
builder(self)
|
||||
} else {
|
||||
unsafe { objc2::msg_send![super(self), inputAccessoryView] }
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe impl NSObjectProtocol for WryWebView {}
|
||||
|
||||
// Drag & Drop
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSDraggingDestination for WryWebView {
|
||||
#[unsafe(method(draggingEntered:))]
|
||||
fn dragging_entered(
|
||||
&self,
|
||||
drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
|
||||
) -> objc2_app_kit::NSDragOperation {
|
||||
drag_drop::dragging_entered(self, drag_info)
|
||||
}
|
||||
|
||||
#[unsafe(method(draggingUpdated:))]
|
||||
fn dragging_updated(
|
||||
&self,
|
||||
drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
|
||||
) -> objc2_app_kit::NSDragOperation {
|
||||
drag_drop::dragging_updated(self, drag_info)
|
||||
}
|
||||
|
||||
#[unsafe(method(performDragOperation:))]
|
||||
fn perform_drag_operation(
|
||||
&self,
|
||||
drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
|
||||
) -> Bool {
|
||||
drag_drop::perform_drag_operation(self, drag_info)
|
||||
}
|
||||
|
||||
#[unsafe(method(draggingExited:))]
|
||||
fn dragging_exited(&self, drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>) {
|
||||
drag_drop::dragging_exited(self, drag_info)
|
||||
}
|
||||
}
|
||||
|
||||
// Synthetic mouse events
|
||||
#[cfg(target_os = "macos")]
|
||||
impl WryWebView {
|
||||
#[unsafe(method(otherMouseDown:))]
|
||||
fn other_mouse_down(&self, event: &NSEvent) {
|
||||
synthetic_mouse_events::other_mouse_down(self, event)
|
||||
}
|
||||
|
||||
#[unsafe(method(otherMouseUp:))]
|
||||
fn other_mouse_up(&self, event: &NSEvent) {
|
||||
synthetic_mouse_events::other_mouse_up(self, event)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Custom Protocol Task Checker
|
||||
impl WryWebView {
|
||||
pub(crate) fn add_custom_task_key(&self, task_id: usize) -> Retained<NSUUID> {
|
||||
let task_uuid = NSUUID::new();
|
||||
self
|
||||
.ivars()
|
||||
.custom_protocol_task_ids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(task_id, task_uuid.clone());
|
||||
task_uuid
|
||||
}
|
||||
pub(crate) fn remove_custom_task_key(&self, task_id: usize) {
|
||||
self
|
||||
.ivars()
|
||||
.custom_protocol_task_ids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&task_id);
|
||||
}
|
||||
pub(crate) fn get_custom_task_uuid(&self, task_id: usize) -> Option<Retained<NSUUID>> {
|
||||
self
|
||||
.ivars()
|
||||
.custom_protocol_task_ids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&task_id)
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{ffi::CStr, panic::AssertUnwindSafe};
|
||||
|
||||
use http::Request;
|
||||
use objc2::{
|
||||
define_class, msg_send,
|
||||
rc::Retained,
|
||||
runtime::{NSObject, ProtocolObject},
|
||||
DeclaredClass, MainThreadOnly,
|
||||
};
|
||||
use objc2_foundation::{ns_string, MainThreadMarker, NSObjectProtocol, NSString};
|
||||
use objc2_web_kit::{WKScriptMessage, WKScriptMessageHandler, WKUserContentController};
|
||||
|
||||
pub const IPC_MESSAGE_HANDLER_NAME: &str = "ipc";
|
||||
|
||||
pub struct WryWebViewDelegateIvars {
|
||||
pub controller: Retained<WKUserContentController>,
|
||||
pub ipc_handler: Box<dyn Fn(Request<String>)>,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSObject))]
|
||||
#[thread_kind = MainThreadOnly]
|
||||
#[ivars = WryWebViewDelegateIvars]
|
||||
pub struct WryWebViewDelegate;
|
||||
|
||||
unsafe impl NSObjectProtocol for WryWebViewDelegate {}
|
||||
|
||||
unsafe impl WKScriptMessageHandler for WryWebViewDelegate {
|
||||
// Function for ipc handler
|
||||
#[unsafe(method(userContentController:didReceiveScriptMessage:))]
|
||||
fn did_receive(
|
||||
this: &WryWebViewDelegate,
|
||||
_controller: &WKUserContentController,
|
||||
msg: &WKScriptMessage,
|
||||
) {
|
||||
// Safety: objc runtime calls are unsafe
|
||||
unsafe {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered();
|
||||
|
||||
let ipc_handler = &this.ivars().ipc_handler;
|
||||
let body = msg.body();
|
||||
if let Ok(body) = body.downcast::<NSString>() {
|
||||
let js_utf8 = body.UTF8String();
|
||||
|
||||
let frame_info = msg.frameInfo();
|
||||
let request = frame_info.request();
|
||||
let url = request.URL().unwrap();
|
||||
let absolute_url = url.absoluteString().unwrap();
|
||||
let url_utf8 = absolute_url.UTF8String();
|
||||
|
||||
if let (Ok(url), Ok(js)) = (
|
||||
CStr::from_ptr(url_utf8).to_str(),
|
||||
CStr::from_ptr(js_utf8).to_str(),
|
||||
) {
|
||||
if let Ok(r) = Request::builder().uri(url).body(js.to_string()) {
|
||||
ipc_handler(r);
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("WebView received invalid IPC request: {}", js);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("WebView received invalid IPC call.");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WryWebViewDelegate {
|
||||
pub fn new(
|
||||
controller: Retained<WKUserContentController>,
|
||||
ipc_handler: Box<dyn Fn(Request<String>)>,
|
||||
mtm: MainThreadMarker,
|
||||
) -> Retained<Self> {
|
||||
let delegate = mtm
|
||||
.alloc::<WryWebViewDelegate>()
|
||||
.set_ivars(WryWebViewDelegateIvars {
|
||||
ipc_handler,
|
||||
controller,
|
||||
});
|
||||
|
||||
let delegate: Retained<Self> = unsafe { msg_send![super(delegate), init] };
|
||||
|
||||
let proto_delegate = ProtocolObject::from_ref(&*delegate);
|
||||
unsafe {
|
||||
// this will increase the retain count of the delegate
|
||||
let _res = objc2::exception::catch(AssertUnwindSafe(|| {
|
||||
delegate
|
||||
.ivars()
|
||||
.controller
|
||||
.addScriptMessageHandler_name(proto_delegate, ns_string!(IPC_MESSAGE_HANDLER_NAME));
|
||||
}));
|
||||
}
|
||||
|
||||
delegate
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2::DefinedClass;
|
||||
use objc2::{define_class, msg_send, rc::Retained, MainThreadOnly};
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_app_kit::{NSApplication, NSEvent, NSView, NSWindow, NSWindowButton};
|
||||
use objc2_foundation::MainThreadMarker;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_foundation::NSRect;
|
||||
#[cfg(target_os = "ios")]
|
||||
use objc2_ui_kit::UIView as NSView;
|
||||
|
||||
pub struct WryWebViewParentIvars {
|
||||
#[cfg(target_os = "macos")]
|
||||
traffic_light_inset: std::cell::Cell<Option<(f64, f64)>>,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSView))]
|
||||
#[ivars = WryWebViewParentIvars]
|
||||
pub struct WryWebViewParent;
|
||||
|
||||
/// Overridden NSView methods.
|
||||
impl WryWebViewParent {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method(keyDown:))]
|
||||
fn key_down(&self, event: &NSEvent) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
unsafe {
|
||||
if let Some(menu) = app.mainMenu() {
|
||||
menu.performKeyEquivalent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method(drawRect:))]
|
||||
fn draw(&self, _dirty_rect: NSRect) {
|
||||
if let Some((x, y)) = self.ivars().traffic_light_inset.get() {
|
||||
unsafe { inset_traffic_lights(&self.window().unwrap(), x, y) };
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WryWebViewParent {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
|
||||
let delegate = WryWebViewParent::alloc(mtm).set_ivars(WryWebViewParentIvars {
|
||||
#[cfg(target_os = "macos")]
|
||||
traffic_light_inset: Default::default(),
|
||||
});
|
||||
unsafe { msg_send![super(delegate), init] }
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_traffic_light_inset(&self, ns_window: &NSWindow, position: dpi::Position) {
|
||||
let scale_factor = NSWindow::backingScaleFactor(ns_window);
|
||||
let position = position.to_logical(scale_factor);
|
||||
self
|
||||
.ivars()
|
||||
.traffic_light_inset
|
||||
.replace(Some((position.x, position.y)));
|
||||
|
||||
unsafe {
|
||||
inset_traffic_lights(ns_window, position.x, position.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub unsafe fn inset_traffic_lights(window: &NSWindow, x: f64, y: f64) {
|
||||
let Some(close) = window.standardWindowButton(NSWindowButton::CloseButton) else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("skipping inset_traffic_lights, close button not found");
|
||||
return;
|
||||
};
|
||||
let Some(miniaturize) = window.standardWindowButton(NSWindowButton::MiniaturizeButton) else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("skipping inset_traffic_lights, miniaturize button not found");
|
||||
return;
|
||||
};
|
||||
let zoom = window.standardWindowButton(NSWindowButton::ZoomButton);
|
||||
|
||||
let title_bar_container_view = close.superview().unwrap().superview().unwrap();
|
||||
|
||||
let close_rect = NSView::frame(&close);
|
||||
let title_bar_frame_height = close_rect.size.height + y;
|
||||
let mut title_bar_rect = NSView::frame(&title_bar_container_view);
|
||||
title_bar_rect.size.height = title_bar_frame_height;
|
||||
title_bar_rect.origin.y = window.frame().size.height - title_bar_frame_height;
|
||||
title_bar_container_view.setFrame(title_bar_rect);
|
||||
|
||||
let space_between = NSView::frame(&miniaturize).origin.x - close_rect.origin.x;
|
||||
|
||||
let mut window_buttons = vec![close, miniaturize];
|
||||
if let Some(zoom) = zoom {
|
||||
window_buttons.push(zoom);
|
||||
}
|
||||
|
||||
for (i, button) in window_buttons.into_iter().enumerate() {
|
||||
let mut rect = NSView::frame(&button);
|
||||
rect.origin.x = x + (i as f64 * space_between);
|
||||
button.setFrameOrigin(rect.origin);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::{cell::RefCell, ptr::null_mut, rc::Rc};
|
||||
|
||||
use block2::Block;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2::DefinedClass;
|
||||
use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly};
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSOpenPanel, NSWindowDelegate};
|
||||
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_foundation::{NSArray, NSURL};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_web_kit::WKOpenPanelParameters;
|
||||
use objc2_web_kit::{
|
||||
WKFrameInfo, WKMediaCaptureType, WKPermissionDecision, WKSecurityOrigin, WKUIDelegate,
|
||||
};
|
||||
|
||||
use crate::{NewWindowFeatures, NewWindowResponse, WryWebView};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct NewWindow {
|
||||
#[allow(dead_code)]
|
||||
ns_window: Retained<objc2_app_kit::NSWindow>,
|
||||
#[allow(dead_code)]
|
||||
webview: Retained<objc2_web_kit::WKWebView>,
|
||||
#[allow(dead_code)]
|
||||
delegate: Retained<WryNSWindowDelegate>,
|
||||
}
|
||||
|
||||
// SAFETY: we are not using the new window at all, just dropping it on another thread
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl Send for NewWindow {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl Drop for NewWindow {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.webview.removeFromSuperview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct WryNSWindowDelegateIvars {
|
||||
on_close: Box<dyn Fn()>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
define_class!(
|
||||
#[unsafe(super(NSObject))]
|
||||
#[thread_kind = MainThreadOnly]
|
||||
#[ivars = WryNSWindowDelegateIvars]
|
||||
struct WryNSWindowDelegate;
|
||||
|
||||
unsafe impl NSObjectProtocol for WryNSWindowDelegate {}
|
||||
|
||||
unsafe impl NSWindowDelegate for WryNSWindowDelegate {
|
||||
#[unsafe(method(windowWillClose:))]
|
||||
unsafe fn will_close(&self, _notification: &objc2_foundation::NSNotification) {
|
||||
let on_close = &self.ivars().on_close;
|
||||
on_close();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl WryNSWindowDelegate {
|
||||
pub fn new(mtm: MainThreadMarker, on_close: Box<dyn Fn()>) -> Retained<Self> {
|
||||
let delegate = mtm
|
||||
.alloc::<WryNSWindowDelegate>()
|
||||
.set_ivars(WryNSWindowDelegateIvars { on_close });
|
||||
unsafe { msg_send![super(delegate), init] }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WryWebViewUIDelegateIvars {
|
||||
#[cfg(target_os = "macos")]
|
||||
new_window_req_handler: Option<Box<dyn Fn(String, NewWindowFeatures) -> NewWindowResponse>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
new_windows: Rc<RefCell<Vec<NewWindow>>>,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSObject))]
|
||||
#[thread_kind = MainThreadOnly]
|
||||
#[ivars = WryWebViewUIDelegateIvars]
|
||||
pub struct WryWebViewUIDelegate;
|
||||
|
||||
unsafe impl NSObjectProtocol for WryWebViewUIDelegate {}
|
||||
|
||||
unsafe impl WKUIDelegate for WryWebViewUIDelegate {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:))]
|
||||
fn run_file_upload_panel(
|
||||
&self,
|
||||
_webview: &WryWebView,
|
||||
open_panel_params: &WKOpenPanelParameters,
|
||||
_frame: &WKFrameInfo,
|
||||
handler: &block2::Block<dyn Fn(*const NSArray<NSURL>)>,
|
||||
) {
|
||||
unsafe {
|
||||
if let Some(mtm) = MainThreadMarker::new() {
|
||||
let open_panel = NSOpenPanel::openPanel(mtm);
|
||||
open_panel.setCanChooseFiles(true);
|
||||
let allow_multi = open_panel_params.allowsMultipleSelection();
|
||||
open_panel.setAllowsMultipleSelection(allow_multi);
|
||||
let allow_dir = open_panel_params.allowsDirectories();
|
||||
open_panel.setCanChooseDirectories(allow_dir);
|
||||
let ok: NSModalResponse = open_panel.runModal();
|
||||
if ok == NSModalResponseOK {
|
||||
let url = open_panel.URLs();
|
||||
(*handler).call((Retained::as_ptr(&url),));
|
||||
} else {
|
||||
(*handler).call((null_mut(),));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(method(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:))]
|
||||
fn request_media_capture_permission(
|
||||
&self,
|
||||
_webview: &WryWebView,
|
||||
_origin: &WKSecurityOrigin,
|
||||
_frame: &WKFrameInfo,
|
||||
_capture_type: WKMediaCaptureType,
|
||||
decision_handler: &Block<dyn Fn(WKPermissionDecision)>,
|
||||
) {
|
||||
//https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc
|
||||
(*decision_handler).call((WKPermissionDecision::Grant,));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method_id(webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:))]
|
||||
unsafe fn create_web_view_for_navigation_action(
|
||||
&self,
|
||||
webview: &WryWebView,
|
||||
configuration: &objc2_web_kit::WKWebViewConfiguration,
|
||||
action: &objc2_web_kit::WKNavigationAction,
|
||||
window_features: &objc2_web_kit::WKWindowFeatures,
|
||||
) -> Option<Retained<objc2_web_kit::WKWebView>> {
|
||||
if let Some(new_window_req_handler) = &self.ivars().new_window_req_handler {
|
||||
let request = action.request();
|
||||
let url = request.URL().unwrap().absoluteString().unwrap();
|
||||
|
||||
let current_window = webview.window().unwrap();
|
||||
let screen = current_window.screen().unwrap();
|
||||
let screen_frame = screen.frame();
|
||||
|
||||
match new_window_req_handler(
|
||||
url.to_string(),
|
||||
NewWindowFeatures {
|
||||
size: if let (Some(width), Some(height)) =
|
||||
(window_features.width(), window_features.height())
|
||||
{
|
||||
Some(dpi::LogicalSize::new(
|
||||
width.doubleValue(),
|
||||
height.doubleValue(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
position: if let (Some(x), Some(y)) = (window_features.x(), window_features.y()) {
|
||||
Some(dpi::LogicalPosition::new(x.doubleValue(), y.doubleValue()))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
opener: crate::NewWindowOpener {
|
||||
webview: webview.into(),
|
||||
target_configuration: configuration.into(),
|
||||
},
|
||||
},
|
||||
) {
|
||||
NewWindowResponse::Allow => {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
|
||||
let defaults = current_window.frame();
|
||||
let size = objc2_foundation::NSSize::new(
|
||||
window_features
|
||||
.width()
|
||||
.map_or(defaults.size.width, |width| width.doubleValue()),
|
||||
window_features
|
||||
.height()
|
||||
.map_or(defaults.size.height, |height| height.doubleValue()),
|
||||
);
|
||||
let position = objc2_foundation::NSPoint::new(
|
||||
window_features
|
||||
.x()
|
||||
.map_or(defaults.origin.x, |x| x.doubleValue()),
|
||||
window_features.y().map_or(defaults.origin.y, |y| {
|
||||
screen_frame.size.height - y.doubleValue() - size.height
|
||||
}),
|
||||
);
|
||||
let rect = objc2_foundation::NSRect::new(position, size);
|
||||
|
||||
let mut flags = objc2_app_kit::NSWindowStyleMask::Titled
|
||||
| objc2_app_kit::NSWindowStyleMask::Closable
|
||||
| objc2_app_kit::NSWindowStyleMask::Miniaturizable;
|
||||
let resizable = window_features
|
||||
.allowsResizing()
|
||||
.map_or(true, |resizable| resizable.boolValue());
|
||||
if resizable {
|
||||
flags |= objc2_app_kit::NSWindowStyleMask::Resizable;
|
||||
}
|
||||
|
||||
let window = objc2_app_kit::NSWindow::initWithContentRect_styleMask_backing_defer(
|
||||
mtm.alloc::<objc2_app_kit::NSWindow>(),
|
||||
rect,
|
||||
flags,
|
||||
objc2_app_kit::NSBackingStoreType::Buffered,
|
||||
false,
|
||||
);
|
||||
|
||||
// SAFETY: Disable auto-release when closing windows.
|
||||
// This is required when creating `NSWindow` outside a window
|
||||
// controller.
|
||||
window.setReleasedWhenClosed(false);
|
||||
|
||||
let webview = objc2_web_kit::WKWebView::initWithFrame_configuration(
|
||||
mtm.alloc::<objc2_web_kit::WKWebView>(),
|
||||
window.frame(),
|
||||
configuration,
|
||||
);
|
||||
|
||||
let new_windows = self.ivars().new_windows.clone();
|
||||
let window_id = Retained::as_ptr(&window) as usize;
|
||||
let delegate = WryNSWindowDelegate::new(
|
||||
mtm,
|
||||
Box::new(move || {
|
||||
new_windows
|
||||
.borrow_mut()
|
||||
.retain(|window| Retained::as_ptr(&window.ns_window) as usize != window_id);
|
||||
}),
|
||||
);
|
||||
window.setDelegate(Some(objc2::runtime::ProtocolObject::from_ref(&*delegate)));
|
||||
|
||||
window.setContentView(Some(&webview));
|
||||
window.makeKeyAndOrderFront(None);
|
||||
|
||||
self.ivars().new_windows.borrow_mut().push(NewWindow {
|
||||
ns_window: window,
|
||||
webview: webview.clone(),
|
||||
delegate,
|
||||
});
|
||||
|
||||
Some(webview)
|
||||
}
|
||||
NewWindowResponse::Create { webview } => Some(webview),
|
||||
NewWindowResponse::Deny => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WryWebViewUIDelegate {
|
||||
pub fn new(
|
||||
mtm: MainThreadMarker,
|
||||
new_window_req_handler: Option<Box<dyn Fn(String, NewWindowFeatures) -> NewWindowResponse>>,
|
||||
) -> Retained<Self> {
|
||||
#[cfg(target_os = "ios")]
|
||||
let _new_window_req_handler = new_window_req_handler;
|
||||
|
||||
let delegate = mtm
|
||||
.alloc::<WryWebViewUIDelegate>()
|
||||
.set_ivars(WryWebViewUIDelegateIvars {
|
||||
#[cfg(target_os = "macos")]
|
||||
new_window_req_handler,
|
||||
#[cfg(target_os = "macos")]
|
||||
new_windows: Rc::new(RefCell::new(vec![])),
|
||||
});
|
||||
unsafe { msg_send![super(delegate), init] }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
use std::{env::current_dir, ptr::null_mut};
|
||||
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject, DeclaredClass};
|
||||
use objc2_foundation::{NSData, NSError, NSString, NSURLResponse, NSURL};
|
||||
use objc2_web_kit::{WKDownload, WKNavigationAction, WKNavigationResponse};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
use crate::wkwebview::ios::WKWebView::WKWebView;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_web_kit::WKWebView;
|
||||
|
||||
use super::class::{
|
||||
wry_download_delegate::WryDownloadDelegate, wry_navigation_delegate::WryNavigationDelegate,
|
||||
};
|
||||
|
||||
// Download action handler
|
||||
pub(crate) fn navigation_download_action(
|
||||
this: &WryNavigationDelegate,
|
||||
_webview: &WKWebView,
|
||||
_action: &WKNavigationAction,
|
||||
download: &WKDownload,
|
||||
) {
|
||||
unsafe {
|
||||
if let Some(delegate) = &this.ivars().download_delegate {
|
||||
let proto_delegate = ProtocolObject::from_ref(&**delegate);
|
||||
download.setDelegate(Some(proto_delegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Download response handler
|
||||
pub(crate) fn navigation_download_response(
|
||||
this: &WryNavigationDelegate,
|
||||
_webview: &WKWebView,
|
||||
_response: &WKNavigationResponse,
|
||||
download: &WKDownload,
|
||||
) {
|
||||
unsafe {
|
||||
if let Some(delegate) = &this.ivars().download_delegate {
|
||||
let proto_delegate = ProtocolObject::from_ref(&**delegate);
|
||||
download.setDelegate(Some(proto_delegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn download_policy(
|
||||
this: &WryDownloadDelegate,
|
||||
download: &WKDownload,
|
||||
_response: &NSURLResponse,
|
||||
suggested_filename: &NSString,
|
||||
completion_handler: &block2::Block<dyn Fn(*const NSURL)>,
|
||||
) {
|
||||
unsafe {
|
||||
let request = download.originalRequest().unwrap();
|
||||
let url = request.URL().unwrap().absoluteString().unwrap();
|
||||
let suggested_filename = suggested_filename.to_string();
|
||||
let mut download_destination =
|
||||
dirs::download_dir().unwrap_or_else(|| current_dir().unwrap_or_default());
|
||||
|
||||
download_destination.push(&suggested_filename);
|
||||
|
||||
let (suggested_filename, ext) = suggested_filename
|
||||
.split_once('.')
|
||||
.map(|(base, ext)| (base, format!(".{ext}")))
|
||||
.unwrap_or((&suggested_filename, "".to_string()));
|
||||
|
||||
// WebView2 does not overwrite files but appends numbers
|
||||
let mut counter = 1;
|
||||
while download_destination.exists() {
|
||||
download_destination.set_file_name(format!("{suggested_filename} ({counter}){ext}"));
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
let started_fn = &this.ivars().started;
|
||||
if let Some(started_fn) = started_fn {
|
||||
let mut started_fn = started_fn.borrow_mut();
|
||||
match started_fn(url.to_string(), &mut download_destination) {
|
||||
true => {
|
||||
let path = NSString::from_str(&download_destination.display().to_string());
|
||||
let ns_url = NSURL::fileURLWithPath_isDirectory(&path, false);
|
||||
(*completion_handler).call((Retained::as_ptr(&ns_url),))
|
||||
}
|
||||
false => (*completion_handler).call((null_mut(),)),
|
||||
};
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("WebView instance is dropped! This navigation handler shouldn't be called.");
|
||||
(*completion_handler).call((null_mut(),));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn download_did_finish(this: &WryDownloadDelegate, download: &WKDownload) {
|
||||
unsafe {
|
||||
let original_request = download.originalRequest().unwrap();
|
||||
let url = original_request.URL().unwrap().absoluteString().unwrap();
|
||||
if let Some(completed_fn) = this.ivars().completed.clone() {
|
||||
completed_fn(url.to_string(), None, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn download_did_fail(
|
||||
this: &WryDownloadDelegate,
|
||||
download: &WKDownload,
|
||||
error: &NSError,
|
||||
_resume_data: &NSData,
|
||||
) {
|
||||
unsafe {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let description = error.localizedDescription().to_string();
|
||||
eprintln!("Download failed with error: {description}");
|
||||
}
|
||||
|
||||
let original_request = download.originalRequest().unwrap();
|
||||
let url = original_request.URL().unwrap().absoluteString().unwrap();
|
||||
if let Some(completed_fn) = this.ivars().completed.clone() {
|
||||
completed_fn(url.to_string(), None, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{ffi::CStr, path::PathBuf};
|
||||
|
||||
use objc2::{
|
||||
runtime::{Bool, ProtocolObject},
|
||||
DeclaredClass,
|
||||
};
|
||||
use objc2_app_kit::{NSDragOperation, NSDraggingInfo, NSFilenamesPboardType};
|
||||
use objc2_foundation::{NSArray, NSPoint, NSRect, NSString};
|
||||
|
||||
use crate::DragDropEvent;
|
||||
|
||||
use super::WryWebView;
|
||||
|
||||
pub(crate) unsafe fn collect_paths(drag_info: &ProtocolObject<dyn NSDraggingInfo>) -> Vec<PathBuf> {
|
||||
let pb = drag_info.draggingPasteboard();
|
||||
let mut drag_drop_paths = Vec::new();
|
||||
let types = NSArray::arrayWithObject(NSFilenamesPboardType);
|
||||
|
||||
if pb.availableTypeFromArray(&types).is_some() {
|
||||
let paths = pb.propertyListForType(NSFilenamesPboardType).unwrap();
|
||||
let paths = paths.downcast::<NSArray>().unwrap();
|
||||
for path in paths {
|
||||
let path = path.downcast::<NSString>().unwrap();
|
||||
let path = CStr::from_ptr(path.UTF8String()).to_string_lossy();
|
||||
drag_drop_paths.push(PathBuf::from(path.into_owned()));
|
||||
}
|
||||
}
|
||||
drag_drop_paths
|
||||
}
|
||||
|
||||
pub(crate) fn dragging_entered(
|
||||
this: &WryWebView,
|
||||
drag_info: &ProtocolObject<dyn NSDraggingInfo>,
|
||||
) -> NSDragOperation {
|
||||
let paths = unsafe { collect_paths(drag_info) };
|
||||
let dl: NSPoint = unsafe { drag_info.draggingLocation() };
|
||||
let frame: NSRect = this.frame();
|
||||
let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
|
||||
|
||||
let listener = &this.ivars().drag_drop_handler;
|
||||
if !listener(DragDropEvent::Enter { paths, position }) {
|
||||
// Reject the Wry file drop (invoke the OS default behaviour)
|
||||
unsafe { objc2::msg_send![super(this), draggingEntered: drag_info] }
|
||||
} else {
|
||||
NSDragOperation::Copy
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dragging_updated(
|
||||
this: &WryWebView,
|
||||
drag_info: &ProtocolObject<dyn NSDraggingInfo>,
|
||||
) -> NSDragOperation {
|
||||
let dl: NSPoint = unsafe { drag_info.draggingLocation() };
|
||||
let frame: NSRect = this.frame();
|
||||
let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
|
||||
|
||||
let listener = &this.ivars().drag_drop_handler;
|
||||
if !listener(DragDropEvent::Over { position }) {
|
||||
unsafe {
|
||||
let os_operation = objc2::msg_send![super(this), draggingUpdated: drag_info];
|
||||
if os_operation == NSDragOperation::None {
|
||||
// 0 will be returned for a drop on any arbitrary location on the webview.
|
||||
// We'll override that with NSDragOperationCopy.
|
||||
NSDragOperation::Copy
|
||||
} else {
|
||||
// A different NSDragOperation is returned when a file is hovered over something like
|
||||
// a <input type="file">, so we'll make sure to preserve that behaviour.
|
||||
os_operation
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSDragOperation::Copy
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn perform_drag_operation(
|
||||
this: &WryWebView,
|
||||
drag_info: &ProtocolObject<dyn NSDraggingInfo>,
|
||||
) -> Bool {
|
||||
let paths = unsafe { collect_paths(drag_info) };
|
||||
let dl: NSPoint = unsafe { drag_info.draggingLocation() };
|
||||
let frame: NSRect = this.frame();
|
||||
let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
|
||||
|
||||
let listener = &this.ivars().drag_drop_handler;
|
||||
if !listener(DragDropEvent::Drop { paths, position }) {
|
||||
// Reject the Wry drop (invoke the OS default behaviour)
|
||||
unsafe { objc2::msg_send![super(this), performDragOperation: drag_info] }
|
||||
} else {
|
||||
Bool::YES
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dragging_exited(this: &WryWebView, drag_info: &ProtocolObject<dyn NSDraggingInfo>) {
|
||||
let listener = &this.ivars().drag_drop_handler;
|
||||
if !listener(DragDropEvent::Leave) {
|
||||
// Reject the Wry drop (invoke the OS default behaviour)
|
||||
unsafe { objc2::msg_send![super(this), draggingExited: drag_info] }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,625 @@
|
|||
//! This file has been imported from `objc2-web-kit` and modified to match iOS.
|
||||
#![allow(warnings)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use std::{ffi::c_double, ptr::NonNull};
|
||||
|
||||
use objc2::{
|
||||
encode::{Encode, Encoding, RefEncode},
|
||||
extern_class, extern_methods,
|
||||
rc::{Allocated, Retained},
|
||||
runtime::{AnyObject, ProtocolObject},
|
||||
MainThreadOnly,
|
||||
};
|
||||
use objc2_core_foundation::*;
|
||||
use objc2_foundation::*;
|
||||
use objc2_ui_kit::*;
|
||||
use objc2_web_kit::*;
|
||||
|
||||
use crate::*;
|
||||
|
||||
// NS_ENUM
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct WKMediaPlaybackState(pub NSInteger);
|
||||
impl WKMediaPlaybackState {
|
||||
#[doc(alias = "WKMediaPlaybackStateNone")]
|
||||
pub const None: Self = Self(0);
|
||||
#[doc(alias = "WKMediaPlaybackStatePlaying")]
|
||||
pub const Playing: Self = Self(1);
|
||||
#[doc(alias = "WKMediaPlaybackStatePaused")]
|
||||
pub const Paused: Self = Self(2);
|
||||
#[doc(alias = "WKMediaPlaybackStateSuspended")]
|
||||
pub const Suspended: Self = Self(3);
|
||||
}
|
||||
|
||||
unsafe impl Encode for WKMediaPlaybackState {
|
||||
const ENCODING: Encoding = NSInteger::ENCODING;
|
||||
}
|
||||
|
||||
unsafe impl RefEncode for WKMediaPlaybackState {
|
||||
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
|
||||
}
|
||||
|
||||
// NS_ENUM
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct WKMediaCaptureState(pub NSInteger);
|
||||
impl WKMediaCaptureState {
|
||||
#[doc(alias = "WKMediaCaptureStateNone")]
|
||||
pub const None: Self = Self(0);
|
||||
#[doc(alias = "WKMediaCaptureStateActive")]
|
||||
pub const Active: Self = Self(1);
|
||||
#[doc(alias = "WKMediaCaptureStateMuted")]
|
||||
pub const Muted: Self = Self(2);
|
||||
}
|
||||
|
||||
unsafe impl Encode for WKMediaCaptureState {
|
||||
const ENCODING: Encoding = NSInteger::ENCODING;
|
||||
}
|
||||
|
||||
unsafe impl RefEncode for WKMediaCaptureState {
|
||||
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
|
||||
}
|
||||
|
||||
// NS_ENUM
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct WKFullscreenState(pub NSInteger);
|
||||
impl WKFullscreenState {
|
||||
#[doc(alias = "WKFullscreenStateNotInFullscreen")]
|
||||
pub const NotInFullscreen: Self = Self(0);
|
||||
#[doc(alias = "WKFullscreenStateEnteringFullscreen")]
|
||||
pub const EnteringFullscreen: Self = Self(1);
|
||||
#[doc(alias = "WKFullscreenStateInFullscreen")]
|
||||
pub const InFullscreen: Self = Self(2);
|
||||
#[doc(alias = "WKFullscreenStateExitingFullscreen")]
|
||||
pub const ExitingFullscreen: Self = Self(3);
|
||||
}
|
||||
|
||||
unsafe impl Encode for WKFullscreenState {
|
||||
const ENCODING: Encoding = NSInteger::ENCODING;
|
||||
}
|
||||
|
||||
unsafe impl RefEncode for WKFullscreenState {
|
||||
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
|
||||
}
|
||||
|
||||
extern_class!(
|
||||
#[unsafe(super(UIView, UIResponder, NSObject))]
|
||||
#[thread_kind = MainThreadOnly]
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct WKWebView;
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSAccessibility for WKWebView {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSAccessibilityElementProtocol for WKWebView {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSAnimatablePropertyContainer for WKWebView {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSAppearanceCustomization for WKWebView {}
|
||||
|
||||
unsafe impl NSCoding for WKWebView {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSDraggingDestination for WKWebView {}
|
||||
|
||||
unsafe impl NSObjectProtocol for WKWebView {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSUserInterfaceItemIdentification for WKWebView {}
|
||||
|
||||
impl WKWebView {
|
||||
extern_methods!(
|
||||
// #[cfg(feature = "WKWebViewConfiguration")]
|
||||
#[unsafe(method(configuration))]
|
||||
pub unsafe fn configuration(&self) -> Retained<WKWebViewConfiguration>;
|
||||
|
||||
// #[cfg(feature = "WKNavigationDelegate")]
|
||||
#[unsafe(method(navigationDelegate))]
|
||||
pub unsafe fn navigationDelegate(
|
||||
&self,
|
||||
) -> Option<Retained<ProtocolObject<dyn WKNavigationDelegate>>>;
|
||||
|
||||
// #[cfg(feature = "WKNavigationDelegate")]
|
||||
#[unsafe(method(setNavigationDelegate:))]
|
||||
pub unsafe fn setNavigationDelegate(
|
||||
&self,
|
||||
navigation_delegate: Option<&ProtocolObject<dyn WKNavigationDelegate>>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "WKUIDelegate")]
|
||||
#[unsafe(method(UIDelegate))]
|
||||
pub unsafe fn UIDelegate(&self) -> Option<Retained<ProtocolObject<dyn WKUIDelegate>>>;
|
||||
|
||||
// #[cfg(feature = "WKUIDelegate")]
|
||||
#[unsafe(method(setUIDelegate:))]
|
||||
pub unsafe fn setUIDelegate(&self, ui_delegate: Option<&ProtocolObject<dyn WKUIDelegate>>);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
// #[cfg(feature = "WKBackForwardList")]
|
||||
#[unsafe(method(backForwardList))]
|
||||
pub unsafe fn backForwardList(&self) -> Retained<WKBackForwardList>;
|
||||
|
||||
// #[cfg(feature = "WKWebViewConfiguration")]
|
||||
#[unsafe(method(initWithFrame:configuration:))]
|
||||
pub unsafe fn initWithFrame_configuration(
|
||||
this: Allocated<Self>,
|
||||
frame: CGRect,
|
||||
configuration: &WKWebViewConfiguration,
|
||||
) -> Retained<Self>;
|
||||
|
||||
#[unsafe(method(initWithCoder:))]
|
||||
pub unsafe fn initWithCoder(this: Allocated<Self>, coder: &NSCoder) -> Option<Retained<Self>>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(loadRequest:))]
|
||||
pub unsafe fn loadRequest(&self, request: &NSURLRequest) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(loadFileURL:allowingReadAccessToURL:))]
|
||||
pub unsafe fn loadFileURL_allowingReadAccessToURL(
|
||||
&self,
|
||||
url: &NSURL,
|
||||
read_access_url: &NSURL,
|
||||
) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(loadHTMLString:baseURL:))]
|
||||
pub unsafe fn loadHTMLString_baseURL(
|
||||
&self,
|
||||
string: &NSString,
|
||||
base_url: Option<&NSURL>,
|
||||
) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(loadData:MIMEType:characterEncodingName:baseURL:))]
|
||||
pub unsafe fn loadData_MIMEType_characterEncodingName_baseURL(
|
||||
&self,
|
||||
data: &NSData,
|
||||
mime_type: &NSString,
|
||||
character_encoding_name: &NSString,
|
||||
base_url: &NSURL,
|
||||
) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
// #[cfg(all(feature = "WKBackForwardListItem", feature = "WKNavigation"))]
|
||||
#[unsafe(method(goToBackForwardListItem:))]
|
||||
pub unsafe fn goToBackForwardListItem(
|
||||
&self,
|
||||
item: &WKBackForwardListItem,
|
||||
) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
#[unsafe(method(title))]
|
||||
pub unsafe fn title(&self) -> Option<Retained<NSString>>;
|
||||
|
||||
#[unsafe(method(URL))]
|
||||
pub unsafe fn URL(&self) -> Option<Retained<NSURL>>;
|
||||
|
||||
#[unsafe(method(isLoading))]
|
||||
pub unsafe fn isLoading(&self) -> bool;
|
||||
|
||||
#[unsafe(method(estimatedProgress))]
|
||||
pub unsafe fn estimatedProgress(&self) -> c_double;
|
||||
|
||||
#[unsafe(method(hasOnlySecureContent))]
|
||||
pub unsafe fn hasOnlySecureContent(&self) -> bool;
|
||||
|
||||
#[unsafe(method(canGoBack))]
|
||||
pub unsafe fn canGoBack(&self) -> bool;
|
||||
|
||||
#[unsafe(method(canGoForward))]
|
||||
pub unsafe fn canGoForward(&self) -> bool;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(goBack))]
|
||||
pub unsafe fn goBack(&self) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(goForward))]
|
||||
pub unsafe fn goForward(&self) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(reload))]
|
||||
pub unsafe fn reload(&self) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(reloadFromOrigin))]
|
||||
pub unsafe fn reloadFromOrigin(&self) -> Option<Retained<WKNavigation>>;
|
||||
|
||||
#[unsafe(method(stopLoading))]
|
||||
pub unsafe fn stopLoading(&self);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[unsafe(method(evaluateJavaScript:completionHandler:))]
|
||||
pub unsafe fn evaluateJavaScript_completionHandler(
|
||||
&self,
|
||||
java_script_string: &NSString,
|
||||
completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
// #[cfg(all(
|
||||
// feature = "WKContentWorld",
|
||||
// feature = "WKFrameInfo",
|
||||
// feature = "block2"
|
||||
// ))]
|
||||
#[unsafe(method(evaluateJavaScript:inFrame:inContentWorld:completionHandler:))]
|
||||
pub unsafe fn evaluateJavaScript_inFrame_inContentWorld_completionHandler(
|
||||
&self,
|
||||
java_script_string: &NSString,
|
||||
frame: Option<&WKFrameInfo>,
|
||||
content_world: &WKContentWorld,
|
||||
completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
// #[cfg(all(
|
||||
// feature = "WKContentWorld",
|
||||
// feature = "WKFrameInfo",
|
||||
// feature = "block2"
|
||||
// ))]
|
||||
#[unsafe(method(callAsyncJavaScript:arguments:inFrame:inContentWorld:completionHandler:))]
|
||||
pub unsafe fn callAsyncJavaScript_arguments_inFrame_inContentWorld_completionHandler(
|
||||
&self,
|
||||
function_body: &NSString,
|
||||
arguments: Option<&NSDictionary<NSString, AnyObject>>,
|
||||
frame: Option<&WKFrameInfo>,
|
||||
content_world: &WKContentWorld,
|
||||
completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[unsafe(method(closeAllMediaPresentationsWithCompletionHandler:))]
|
||||
pub unsafe fn closeAllMediaPresentationsWithCompletionHandler(
|
||||
&self,
|
||||
completion_handler: Option<&block2::Block<dyn Fn()>>,
|
||||
);
|
||||
|
||||
#[deprecated]
|
||||
#[unsafe(method(closeAllMediaPresentations))]
|
||||
pub unsafe fn closeAllMediaPresentations(&self);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[unsafe(method(pauseAllMediaPlaybackWithCompletionHandler:))]
|
||||
pub unsafe fn pauseAllMediaPlaybackWithCompletionHandler(
|
||||
&self,
|
||||
completion_handler: Option<&block2::Block<dyn Fn()>>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[deprecated]
|
||||
#[unsafe(method(pauseAllMediaPlayback:))]
|
||||
pub unsafe fn pauseAllMediaPlayback(
|
||||
&self,
|
||||
completion_handler: Option<&block2::Block<dyn Fn()>>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[unsafe(method(setAllMediaPlaybackSuspended:completionHandler:))]
|
||||
pub unsafe fn setAllMediaPlaybackSuspended_completionHandler(
|
||||
&self,
|
||||
suspended: bool,
|
||||
completion_handler: Option<&block2::Block<dyn Fn()>>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[deprecated]
|
||||
#[unsafe(method(resumeAllMediaPlayback:))]
|
||||
pub unsafe fn resumeAllMediaPlayback(
|
||||
&self,
|
||||
completion_handler: Option<&block2::Block<dyn Fn()>>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[deprecated]
|
||||
#[unsafe(method(suspendAllMediaPlayback:))]
|
||||
pub unsafe fn suspendAllMediaPlayback(
|
||||
&self,
|
||||
completion_handler: Option<&block2::Block<dyn Fn()>>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[unsafe(method(requestMediaPlaybackStateWithCompletionHandler:))]
|
||||
pub unsafe fn requestMediaPlaybackStateWithCompletionHandler(
|
||||
&self,
|
||||
completion_handler: &block2::Block<dyn Fn(WKMediaPlaybackState)>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[deprecated]
|
||||
#[unsafe(method(requestMediaPlaybackState:))]
|
||||
pub unsafe fn requestMediaPlaybackState(
|
||||
&self,
|
||||
completion_handler: &block2::Block<dyn Fn(WKMediaPlaybackState)>,
|
||||
);
|
||||
|
||||
#[unsafe(method(cameraCaptureState))]
|
||||
pub unsafe fn cameraCaptureState(&self) -> WKMediaCaptureState;
|
||||
|
||||
#[unsafe(method(microphoneCaptureState))]
|
||||
pub unsafe fn microphoneCaptureState(&self) -> WKMediaCaptureState;
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[unsafe(method(setCameraCaptureState:completionHandler:))]
|
||||
pub unsafe fn setCameraCaptureState_completionHandler(
|
||||
&self,
|
||||
state: WKMediaCaptureState,
|
||||
completion_handler: Option<&block2::Block<dyn Fn()>>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[unsafe(method(setMicrophoneCaptureState:completionHandler:))]
|
||||
pub unsafe fn setMicrophoneCaptureState_completionHandler(
|
||||
&self,
|
||||
state: WKMediaCaptureState,
|
||||
completion_handler: Option<&block2::Block<dyn Fn()>>,
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
// #[cfg(all(feature = "WKSnapshotConfiguration", feature = "block2"))]
|
||||
#[unsafe(method(takeSnapshotWithConfiguration:completionHandler:))]
|
||||
pub unsafe fn takeSnapshotWithConfiguration_completionHandler(
|
||||
&self,
|
||||
snapshot_configuration: Option<&WKSnapshotConfiguration>,
|
||||
completion_handler: &block2::Block<dyn Fn(*mut NSImage, *mut NSError)>,
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
// #[cfg(all(feature = "WKPDFConfiguration", feature = "block2"))]
|
||||
#[unsafe(method(createPDFWithConfiguration:completionHandler:))]
|
||||
pub unsafe fn createPDFWithConfiguration_completionHandler(
|
||||
&self,
|
||||
pdf_configuration: Option<&WKPDFConfiguration>,
|
||||
completion_handler: &block2::Block<dyn Fn(*mut NSData, *mut NSError)>,
|
||||
);
|
||||
|
||||
// #[cfg(feature = "block2")]
|
||||
#[unsafe(method(createWebArchiveDataWithCompletionHandler:))]
|
||||
pub unsafe fn createWebArchiveDataWithCompletionHandler(
|
||||
&self,
|
||||
completion_handler: &block2::Block<dyn Fn(NonNull<NSData>, NonNull<NSError>)>,
|
||||
);
|
||||
|
||||
#[unsafe(method(allowsBackForwardNavigationGestures))]
|
||||
pub unsafe fn allowsBackForwardNavigationGestures(&self) -> bool;
|
||||
|
||||
#[unsafe(method(setAllowsBackForwardNavigationGestures:))]
|
||||
pub unsafe fn setAllowsBackForwardNavigationGestures(
|
||||
&self,
|
||||
allows_back_forward_navigation_gestures: bool,
|
||||
);
|
||||
|
||||
#[unsafe(method(customUserAgent))]
|
||||
pub unsafe fn customUserAgent(&self) -> Option<Retained<NSString>>;
|
||||
|
||||
#[unsafe(method(setCustomUserAgent:))]
|
||||
pub unsafe fn setCustomUserAgent(&self, custom_user_agent: Option<&NSString>);
|
||||
|
||||
#[unsafe(method(allowsLinkPreview))]
|
||||
pub unsafe fn allowsLinkPreview(&self) -> bool;
|
||||
|
||||
#[unsafe(method(setAllowsLinkPreview:))]
|
||||
pub unsafe fn setAllowsLinkPreview(&self, allows_link_preview: bool);
|
||||
|
||||
#[unsafe(method(allowsMagnification))]
|
||||
pub unsafe fn allowsMagnification(&self) -> bool;
|
||||
|
||||
#[unsafe(method(setAllowsMagnification:))]
|
||||
pub unsafe fn setAllowsMagnification(&self, allows_magnification: bool);
|
||||
|
||||
#[unsafe(method(magnification))]
|
||||
pub unsafe fn magnification(&self) -> CGFloat;
|
||||
|
||||
#[unsafe(method(setMagnification:))]
|
||||
pub unsafe fn setMagnification(&self, magnification: CGFloat);
|
||||
|
||||
#[unsafe(method(setMagnification:centeredAtPoint:))]
|
||||
pub unsafe fn setMagnification_centeredAtPoint(&self, magnification: CGFloat, point: CGPoint);
|
||||
|
||||
#[unsafe(method(pageZoom))]
|
||||
pub unsafe fn pageZoom(&self) -> CGFloat;
|
||||
|
||||
#[unsafe(method(setPageZoom:))]
|
||||
pub unsafe fn setPageZoom(&self, page_zoom: CGFloat);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
// #[cfg(all(
|
||||
// feature = "WKFindConfiguration",
|
||||
// feature = "WKFindResult",
|
||||
// feature = "block2"
|
||||
// ))]
|
||||
#[unsafe(method(findString:withConfiguration:completionHandler:))]
|
||||
pub unsafe fn findString_withConfiguration_completionHandler(
|
||||
&self,
|
||||
string: &NSString,
|
||||
configuration: Option<&WKFindConfiguration>,
|
||||
completion_handler: &block2::Block<dyn Fn(NonNull<WKFindResult>)>,
|
||||
);
|
||||
|
||||
#[unsafe(method(handlesURLScheme:))]
|
||||
pub unsafe fn handlesURLScheme(url_scheme: &NSString, mtm: MainThreadMarker) -> bool;
|
||||
|
||||
// #[cfg(all(feature = "WKDownload", feature = "block2"))]
|
||||
#[unsafe(method(startDownloadUsingRequest:completionHandler:))]
|
||||
pub unsafe fn startDownloadUsingRequest_completionHandler(
|
||||
&self,
|
||||
request: &NSURLRequest,
|
||||
completion_handler: &block2::Block<dyn Fn(NonNull<WKDownload>)>,
|
||||
);
|
||||
|
||||
// #[cfg(all(feature = "WKDownload", feature = "block2"))]
|
||||
#[unsafe(method(resumeDownloadFromResumeData:completionHandler:))]
|
||||
pub unsafe fn resumeDownloadFromResumeData_completionHandler(
|
||||
&self,
|
||||
resume_data: &NSData,
|
||||
completion_handler: &block2::Block<dyn Fn(NonNull<WKDownload>)>,
|
||||
);
|
||||
|
||||
#[unsafe(method(mediaType))]
|
||||
pub unsafe fn mediaType(&self) -> Option<Retained<NSString>>;
|
||||
|
||||
#[unsafe(method(setMediaType:))]
|
||||
pub unsafe fn setMediaType(&self, media_type: Option<&NSString>);
|
||||
|
||||
#[unsafe(method(interactionState))]
|
||||
pub unsafe fn interactionState(&self) -> Option<Retained<AnyObject>>;
|
||||
|
||||
#[unsafe(method(setInteractionState:))]
|
||||
pub unsafe fn setInteractionState(&self, interaction_state: Option<&AnyObject>);
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(loadSimulatedRequest:response:responseData:))]
|
||||
pub unsafe fn loadSimulatedRequest_response_responseData(
|
||||
&self,
|
||||
request: &NSURLRequest,
|
||||
response: &NSURLResponse,
|
||||
data: &NSData,
|
||||
) -> Retained<WKNavigation>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[deprecated]
|
||||
#[unsafe(method(loadSimulatedRequest:withResponse:responseData:))]
|
||||
pub unsafe fn loadSimulatedRequest_withResponse_responseData(
|
||||
&self,
|
||||
request: &NSURLRequest,
|
||||
response: &NSURLResponse,
|
||||
data: &NSData,
|
||||
) -> Retained<WKNavigation>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(loadFileRequest:allowingReadAccessToURL:))]
|
||||
pub unsafe fn loadFileRequest_allowingReadAccessToURL(
|
||||
&self,
|
||||
request: &NSURLRequest,
|
||||
read_access_url: &NSURL,
|
||||
) -> Retained<WKNavigation>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[unsafe(method(loadSimulatedRequest:responseHTMLString:))]
|
||||
pub unsafe fn loadSimulatedRequest_responseHTMLString(
|
||||
&self,
|
||||
request: &NSURLRequest,
|
||||
string: &NSString,
|
||||
) -> Retained<WKNavigation>;
|
||||
|
||||
// #[cfg(feature = "WKNavigation")]
|
||||
#[deprecated]
|
||||
#[unsafe(method(loadSimulatedRequest:withResponseHTMLString:))]
|
||||
pub unsafe fn loadSimulatedRequest_withResponseHTMLString(
|
||||
&self,
|
||||
request: &NSURLRequest,
|
||||
string: &NSString,
|
||||
) -> Retained<WKNavigation>;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method(printOperationWithPrintInfo:))]
|
||||
pub unsafe fn printOperationWithPrintInfo(
|
||||
&self,
|
||||
print_info: &NSPrintInfo,
|
||||
) -> Retained<NSPrintOperation>;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method(themeColor))]
|
||||
pub unsafe fn themeColor(&self) -> Option<Retained<NSColor>>;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method(underPageBackgroundColor))]
|
||||
pub unsafe fn underPageBackgroundColor(&self) -> Retained<NSColor>;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[unsafe(method(setUnderPageBackgroundColor:))]
|
||||
pub unsafe fn setUnderPageBackgroundColor(&self, under_page_background_color: Option<&NSColor>);
|
||||
|
||||
#[unsafe(method(fullscreenState))]
|
||||
pub unsafe fn fullscreenState(&self) -> WKFullscreenState;
|
||||
|
||||
#[unsafe(method(minimumViewportInset))]
|
||||
pub unsafe fn minimumViewportInset(&self) -> NSEdgeInsets;
|
||||
|
||||
#[unsafe(method(maximumViewportInset))]
|
||||
pub unsafe fn maximumViewportInset(&self) -> NSEdgeInsets;
|
||||
|
||||
#[unsafe(method(setMinimumViewportInset:maximumViewportInset:))]
|
||||
pub unsafe fn setMinimumViewportInset_maximumViewportInset(
|
||||
&self,
|
||||
minimum_viewport_inset: NSEdgeInsets,
|
||||
maximum_viewport_inset: NSEdgeInsets,
|
||||
);
|
||||
|
||||
#[unsafe(method(isInspectable))]
|
||||
pub unsafe fn isInspectable(&self) -> bool;
|
||||
|
||||
#[unsafe(method(setInspectable:))]
|
||||
pub unsafe fn setInspectable(&self, inspectable: bool);
|
||||
);
|
||||
}
|
||||
|
||||
/// Methods declared on superclass `UIView`
|
||||
impl WKWebView {
|
||||
extern_methods!(
|
||||
#[unsafe(method(initWithFrame:))]
|
||||
pub unsafe fn initWithFrame(this: Allocated<Self>, frame_rect: NSRect) -> Retained<Self>;
|
||||
);
|
||||
}
|
||||
|
||||
/// Methods declared on superclass `UIResponder`
|
||||
impl WKWebView {
|
||||
extern_methods!(
|
||||
#[unsafe(method(init))]
|
||||
pub unsafe fn init(this: Allocated<Self>) -> Retained<Self>;
|
||||
);
|
||||
}
|
||||
|
||||
/// Methods declared on superclass `NSObject`
|
||||
impl WKWebView {
|
||||
extern_methods!(
|
||||
#[unsafe(method(new))]
|
||||
pub unsafe fn new(mtm: MainThreadMarker) -> Retained<Self>;
|
||||
);
|
||||
}
|
||||
|
||||
/// WKIBActions
|
||||
impl WKWebView {
|
||||
extern_methods!(
|
||||
#[unsafe(method(goBack:))]
|
||||
pub unsafe fn goBack_(&self, sender: Option<&AnyObject>);
|
||||
|
||||
#[unsafe(method(goForward:))]
|
||||
pub unsafe fn goForward_(&self, sender: Option<&AnyObject>);
|
||||
|
||||
#[unsafe(method(reload:))]
|
||||
pub unsafe fn reload_(&self, sender: Option<&AnyObject>);
|
||||
|
||||
#[unsafe(method(reloadFromOrigin:))]
|
||||
pub unsafe fn reloadFromOrigin_(&self, sender: Option<&AnyObject>);
|
||||
|
||||
#[unsafe(method(stopLoading:))]
|
||||
pub unsafe fn stopLoading_(&self, sender: Option<&AnyObject>);
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSUserInterfaceValidations for WKWebView {}
|
||||
|
||||
/// WKNSTextFinderClient
|
||||
impl WKWebView {
|
||||
extern_methods!();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl NSTextFinderClient for WKWebView {}
|
||||
|
||||
/// WKDeprecated
|
||||
impl WKWebView {
|
||||
extern_methods!(
|
||||
#[deprecated]
|
||||
#[unsafe(method(certificateChain))]
|
||||
pub unsafe fn certificateChain(&self) -> Retained<NSArray>;
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
pub mod WKWebView;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,116 @@
|
|||
use objc2::DeclaredClass;
|
||||
use objc2_foundation::{NSObjectProtocol, NSString};
|
||||
use objc2_web_kit::{
|
||||
WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationResponse,
|
||||
WKNavigationResponsePolicy,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
use crate::wkwebview::ios::WKWebView::WKWebView;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_web_kit::WKWebView;
|
||||
|
||||
use crate::PageLoadEvent;
|
||||
|
||||
use super::class::wry_navigation_delegate::WryNavigationDelegate;
|
||||
|
||||
pub(crate) fn did_commit_navigation(
|
||||
this: &WryNavigationDelegate,
|
||||
webview: &WKWebView,
|
||||
_navigation: &WKNavigation,
|
||||
) {
|
||||
unsafe {
|
||||
// Call on_load_handler
|
||||
if let Some(on_page_load) = &this.ivars().on_page_load_handler {
|
||||
on_page_load(PageLoadEvent::Started);
|
||||
}
|
||||
|
||||
// Inject scripts
|
||||
let mut pending_scripts = this.ivars().pending_scripts.lock().unwrap();
|
||||
if let Some(scripts) = &*pending_scripts {
|
||||
for script in scripts {
|
||||
webview.evaluateJavaScript_completionHandler(&NSString::from_str(script), None);
|
||||
}
|
||||
*pending_scripts = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn did_finish_navigation(
|
||||
this: &WryNavigationDelegate,
|
||||
_webview: &WKWebView,
|
||||
_navigation: &WKNavigation,
|
||||
) {
|
||||
if let Some(on_page_load) = &this.ivars().on_page_load_handler {
|
||||
on_page_load(PageLoadEvent::Finished);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation handler
|
||||
pub(crate) fn navigation_policy(
|
||||
this: &WryNavigationDelegate,
|
||||
_webview: &WKWebView,
|
||||
action: &WKNavigationAction,
|
||||
handler: &block2::Block<dyn Fn(WKNavigationActionPolicy)>,
|
||||
) {
|
||||
unsafe {
|
||||
// <https://developer.apple.com/documentation/webkit/wknavigationaction/shouldperformdownload>
|
||||
// Available: macOS 11.3+, iOS 14.5+
|
||||
let can_download = action.respondsToSelector(objc2::sel!(shouldPerformDownload));
|
||||
let should_download: bool = if can_download {
|
||||
action.shouldPerformDownload()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let request = action.request();
|
||||
let url = request.URL().unwrap().absoluteString().unwrap();
|
||||
|
||||
if should_download {
|
||||
let has_download_handler = this.ivars().has_download_handler;
|
||||
if has_download_handler {
|
||||
(*handler).call((WKNavigationActionPolicy::Download,));
|
||||
} else {
|
||||
(*handler).call((WKNavigationActionPolicy::Cancel,));
|
||||
}
|
||||
} else {
|
||||
let function = &this.ivars().navigation_policy_function;
|
||||
match function(url.to_string()) {
|
||||
true => (*handler).call((WKNavigationActionPolicy::Allow,)),
|
||||
false => (*handler).call((WKNavigationActionPolicy::Cancel,)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation handler
|
||||
pub(crate) fn navigation_policy_response(
|
||||
this: &WryNavigationDelegate,
|
||||
_webview: &WKWebView,
|
||||
response: &WKNavigationResponse,
|
||||
handler: &block2::Block<dyn Fn(WKNavigationResponsePolicy)>,
|
||||
) {
|
||||
unsafe {
|
||||
let can_show_mime_type = response.canShowMIMEType();
|
||||
|
||||
if !can_show_mime_type {
|
||||
let has_download_handler = this.ivars().has_download_handler;
|
||||
if has_download_handler {
|
||||
(*handler).call((WKNavigationResponsePolicy::Download,));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
(*handler).call((WKNavigationResponsePolicy::Allow,));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn web_content_process_did_terminate(
|
||||
this: &WryNavigationDelegate,
|
||||
_webview: &WKWebView,
|
||||
) {
|
||||
if let Some(on_web_content_process_terminate) =
|
||||
&this.ivars().on_web_content_process_terminate_handler
|
||||
{
|
||||
on_web_content_process_terminate();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use objc2_foundation::NSObject;
|
||||
use std::ffi::{c_char, CString};
|
||||
|
||||
use crate::{proxy::ProxyEndpoint, Error};
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
pub type nw_endpoint_t = *mut NSObject;
|
||||
#[allow(non_camel_case_types)]
|
||||
pub type nw_protocol_options_t = *mut NSObject;
|
||||
#[allow(non_camel_case_types)]
|
||||
pub type nw_proxy_config_t = *mut NSObject;
|
||||
|
||||
#[link(name = "Network", kind = "framework")]
|
||||
extern "C" {
|
||||
fn nw_endpoint_create_host(host: *const c_char, port: *const c_char) -> nw_endpoint_t;
|
||||
pub fn nw_proxy_config_create_socksv5(proxy_endpoint: nw_endpoint_t) -> nw_proxy_config_t;
|
||||
pub fn nw_proxy_config_create_http_connect(
|
||||
proxy_endpoint: nw_endpoint_t,
|
||||
proxy_tls_options: nw_protocol_options_t,
|
||||
) -> nw_proxy_config_t;
|
||||
}
|
||||
|
||||
impl TryFrom<ProxyEndpoint> for nw_endpoint_t {
|
||||
type Error = Error;
|
||||
fn try_from(endpoint: ProxyEndpoint) -> Result<Self, Error> {
|
||||
unsafe {
|
||||
let endpoint_host =
|
||||
CString::new(endpoint.host).map_err(|_| Error::ProxyEndpointCreationFailed)?;
|
||||
let endpoint_port =
|
||||
CString::new(endpoint.port).map_err(|_| Error::ProxyEndpointCreationFailed)?;
|
||||
let endpoint = nw_endpoint_create_host(endpoint_host.as_ptr(), endpoint_port.as_ptr());
|
||||
|
||||
if endpoint.is_null() {
|
||||
Err(Error::ProxyEndpointCreationFailed)
|
||||
} else {
|
||||
Ok(endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
use objc2_app_kit::{
|
||||
NSAlternateKeyMask, NSCommandKeyMask, NSControlKeyMask, NSEvent, NSEventType, NSShiftKeyMask,
|
||||
NSView,
|
||||
};
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
use super::WryWebView;
|
||||
|
||||
pub(crate) fn other_mouse_down(this: &WryWebView, event: &NSEvent) {
|
||||
unsafe {
|
||||
if event.r#type() == NSEventType::OtherMouseDown {
|
||||
let button_number = event.buttonNumber();
|
||||
match button_number {
|
||||
// back button
|
||||
3 => {
|
||||
let js = create_js_mouse_event(this, event, true, true);
|
||||
this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
|
||||
return;
|
||||
}
|
||||
// forward button
|
||||
4 => {
|
||||
let js = create_js_mouse_event(this, event, true, false);
|
||||
this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
this.mouseDown(event);
|
||||
}
|
||||
}
|
||||
pub(crate) fn other_mouse_up(this: &WryWebView, event: &NSEvent) {
|
||||
unsafe {
|
||||
if event.r#type() == NSEventType::OtherMouseUp {
|
||||
let button_number = event.buttonNumber();
|
||||
match button_number {
|
||||
// back button
|
||||
3 => {
|
||||
let js = create_js_mouse_event(this, event, false, true);
|
||||
this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
|
||||
return;
|
||||
}
|
||||
// forward button
|
||||
4 => {
|
||||
let js = create_js_mouse_event(this, event, false, false);
|
||||
this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
this.mouseUp(event);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_js_mouse_event(
|
||||
view: &NSView,
|
||||
event: &NSEvent,
|
||||
down: bool,
|
||||
back_button: bool,
|
||||
) -> String {
|
||||
let event_name = if down { "mousedown" } else { "mouseup" };
|
||||
// js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
|
||||
let button = if back_button { 3 } else { 4 };
|
||||
let mods_flags = event.modifierFlags();
|
||||
let window_point = event.locationInWindow();
|
||||
let view_point = view.convertPoint_fromView(window_point, None);
|
||||
let x = view_point.x as u32;
|
||||
let y = view_point.y as u32;
|
||||
// js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
|
||||
let buttons = NSEvent::pressedMouseButtons();
|
||||
|
||||
format!(
|
||||
r#"(() => {{
|
||||
const el = document.elementFromPoint({x},{y});
|
||||
const ev = new MouseEvent('{event_name}', {{
|
||||
view: window,
|
||||
button: {button},
|
||||
buttons: {buttons},
|
||||
x: {x},
|
||||
y: {y},
|
||||
bubbles: true,
|
||||
detail: {detail},
|
||||
cancelBubble: false,
|
||||
cancelable: true,
|
||||
clientX: {x},
|
||||
clientY: {y},
|
||||
composed: true,
|
||||
layerX: {x},
|
||||
layerY: {y},
|
||||
pageX: {x},
|
||||
pageY: {y},
|
||||
screenX: window.screenX + {x},
|
||||
screenY: window.screenY + {y},
|
||||
ctrlKey: {ctrl_key},
|
||||
metaKey: {meta_key},
|
||||
shiftKey: {shift_key},
|
||||
altKey: {alt_key},
|
||||
}});
|
||||
el.dispatchEvent(ev)
|
||||
if (!ev.defaultPrevented && "{event_name}" === "mouseup") {{
|
||||
if (ev.button === 3) {{
|
||||
window.history.back();
|
||||
}}
|
||||
if (ev.button === 4) {{
|
||||
window.history.forward();
|
||||
}}
|
||||
}}
|
||||
}})()"#,
|
||||
event_name = event_name,
|
||||
x = x,
|
||||
y = y,
|
||||
detail = event.clickCount(),
|
||||
ctrl_key = mods_flags.contains(NSControlKeyMask),
|
||||
alt_key = mods_flags.contains(NSAlternateKeyMask),
|
||||
shift_key = mods_flags.contains(NSShiftKeyMask),
|
||||
meta_key = mods_flags.contains(NSCommandKeyMask),
|
||||
button = button,
|
||||
buttons = buttons,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use objc2_foundation::NSProcessInfo;
|
||||
|
||||
pub fn operating_system_version() -> (isize, isize, isize) {
|
||||
let process_info = NSProcessInfo::processInfo();
|
||||
let version = process_info.operatingSystemVersion();
|
||||
(
|
||||
version.majorVersion,
|
||||
version.minorVersion,
|
||||
version.patchVersion,
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue