├── src ├── wkwebview │ ├── ios │ │ └── mod.rs │ ├── class │ │ ├── mod.rs │ │ ├── wry_download_delegate.rs │ │ ├── document_title_changed_observer.rs │ │ ├── wry_web_view_delegate.rs │ │ ├── wry_web_view_parent.rs │ │ ├── wry_web_view.rs │ │ └── wry_navigation_delegate.rs │ ├── util.rs │ ├── proxy.rs │ ├── navigation.rs │ ├── drag_drop.rs │ ├── synthetic_mouse_events.rs │ └── download.rs ├── util.rs ├── proxy.rs ├── android │ └── kotlin │ │ ├── proguard-wry.pro │ │ ├── Ipc.kt │ │ ├── Logger.kt │ │ ├── RustWebView.kt │ │ ├── RustWebViewClient.kt │ │ ├── PermissionHelper.kt │ │ └── WryActivity.kt ├── error.rs ├── web_context.rs ├── webview2 │ ├── util.rs │ └── drag_drop.rs └── webkitgtk │ ├── drag_drop.rs │ └── synthetic_mouse_events.rs ├── .github ├── splash.png ├── CODEOWNERS ├── FUNDING.yml ├── workflows │ ├── covector-status.yml │ ├── audit.yml │ ├── covector-comment-on-fork.yml │ ├── clippy-fmt.yml │ ├── covector-version-or-publish.yml │ ├── bench.yml │ └── build.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── CODE_OF_CONDUCT.md ├── examples ├── custom_protocol │ ├── wasm.wasm │ ├── subpage.html │ ├── index.html │ └── script.js ├── streaming │ └── index.html ├── cookies.rs ├── simple.rs ├── transparent.rs ├── winit.rs ├── reparent.rs ├── window_border.rs ├── custom_protocol.rs ├── async_custom_protocol.rs ├── multiwindow.rs ├── gtk_multiwebview.rs ├── multiwebview.rs ├── wgpu.rs └── streaming.rs ├── .changes ├── web-content-process-termination.md ├── OnBackPressedCallback.md ├── readme.md └── config.json ├── audits └── Radically_Open_Security-v1-report.pdf ├── .cargo └── config.toml ├── .license_template ├── bench ├── tests │ ├── src │ │ ├── static │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── worker.js │ │ │ └── site.js │ │ ├── hello_world.rs │ │ ├── custom_protocol.rs │ │ └── cpu_intensive.rs │ └── Cargo.toml ├── Cargo.toml └── src │ ├── build_benchmark_jsons.rs │ ├── utils.rs │ └── run_benchmark.rs ├── renovate.json ├── .gitignore ├── rustfmt.toml ├── SECURITY.md ├── LICENSE.spdx ├── LICENSE-MIT ├── wry-logo.svg └── Cargo.toml /src/wkwebview/ios/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod WKWebView; 2 | -------------------------------------------------------------------------------- /.github/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tauri-apps/wry/HEAD/.github/splash.png -------------------------------------------------------------------------------- /examples/custom_protocol/wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tauri-apps/wry/HEAD/examples/custom_protocol/wasm.wasm -------------------------------------------------------------------------------- /.changes/web-content-process-termination.md: -------------------------------------------------------------------------------- 1 | --- 2 | "wry": minor 3 | --- 4 | 5 | Add handler for web content process termination. 6 | -------------------------------------------------------------------------------- /audits/Radically_Open_Security-v1-report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tauri-apps/wry/HEAD/audits/Radically_Open_Security-v1-report.pdf -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # [build] 2 | # target = "aarch64-linux-android" 3 | [target.x86_64-apple-darwin] 4 | rustflags = ["-C", "link-arg=-mmacosx-version-min=10.12"] 5 | -------------------------------------------------------------------------------- /.changes/OnBackPressedCallback.md: -------------------------------------------------------------------------------- 1 | --- 2 | "wry": patch 3 | --- 4 | 5 | Use OnBackPressedCallback instead of the deprecated onKeyDown for back navigation on Android. 6 | -------------------------------------------------------------------------------- /.license_template: -------------------------------------------------------------------------------- 1 | // Copyright {20\d{2}(-20\d{2})?} Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT -------------------------------------------------------------------------------- /bench/tests/src/static/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, 3 | Arial, sans-serif; 4 | margin: auto; 5 | max-width: 38rem; 6 | padding: 2rem; 7 | } 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:recommended"], 3 | "rangeStrategy": "replace", 4 | "packageRules": [ 5 | { 6 | "semanticCommitType": "chore", 7 | "matchPackageNames": ["*"] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | target 6 | gh-pages 7 | .DS_Store 8 | examples/test_video.mp4 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Current WG Code Sub Teams: 2 | # @tauri-apps/wg-webview 3 | # @tauri-apps/wg-devops 4 | 5 | # Order is important; the last matching pattern takes the most precedence. 6 | * @tauri-apps/wg-webview 7 | 8 | .github @tauri-apps/wg-devops 9 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicU32, Ordering}; 2 | 3 | pub struct Counter(AtomicU32); 4 | 5 | impl Counter { 6 | pub const fn new() -> Self { 7 | Self(AtomicU32::new(1)) 8 | } 9 | 10 | pub fn next(&self) -> u32 { 11 | self.0.fetch_add(1, Ordering::Relaxed) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # 4 | patreon: # 5 | open_collective: tauri 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 2 4 | newline_style = "Unix" 5 | use_small_heuristics = "Default" 6 | reorder_imports = true 7 | reorder_modules = true 8 | remove_nested_parens = true 9 | edition = "2018" 10 | merge_derives = true 11 | use_try_shorthand = false 12 | use_field_init_shorthand = false 13 | force_explicit_abi = true 14 | imports_granularity = "Crate" 15 | #license_template_path = ".license_template" 16 | -------------------------------------------------------------------------------- /src/wkwebview/class/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | pub mod document_title_changed_observer; 6 | pub mod url_scheme_handler; 7 | pub mod wry_download_delegate; 8 | pub mod wry_navigation_delegate; 9 | pub mod wry_web_view; 10 | pub mod wry_web_view_delegate; 11 | pub mod wry_web_view_parent; 12 | pub mod wry_web_view_ui_delegate; 13 | -------------------------------------------------------------------------------- /bench/tests/src/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World! 6 | 7 | 8 | 9 |

Calculate prime numbers

10 |

11 | 12 |

13 |

14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/proxy.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct ProxyEndpoint { 3 | /// Proxy server host (e.g. 192.168.0.100, localhost, example.com, etc.) 4 | pub host: String, 5 | /// Proxy server port (e.g. 1080, 3128, etc.) 6 | pub port: String, 7 | } 8 | 9 | #[derive(Debug, Clone)] 10 | pub enum ProxyConfig { 11 | /// Connect to proxy server via HTTP CONNECT 12 | Http(ProxyEndpoint), 13 | /// Connect to proxy server via SOCKSv5 14 | Socks5(ProxyEndpoint), 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/covector-status.yml: -------------------------------------------------------------------------------- 1 | name: covector status 2 | on: [pull_request] 3 | 4 | jobs: 5 | covector: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 0 12 | - name: covector status 13 | uses: jbolda/covector/packages/action@covector-v0 14 | with: 15 | command: "status" 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | comment: true 18 | -------------------------------------------------------------------------------- /src/wkwebview/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use objc2_foundation::NSProcessInfo; 6 | 7 | pub fn operating_system_version() -> (isize, isize, isize) { 8 | let process_info = NSProcessInfo::processInfo(); 9 | let version = process_info.operatingSystemVersion(); 10 | ( 11 | version.majorVersion, 12 | version.minorVersion, 13 | version.patchVersion, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/custom_protocol/subpage.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Page 2

16 | Back home 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /bench/tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | workspace = {} 2 | 3 | [package] 4 | name = "hello_world" 5 | version = "0.1.0" 6 | description = "A very simple WRY Appplication" 7 | edition = "2018" 8 | 9 | [dependencies] 10 | wry = { path = "../../" } 11 | serde = { version = "1.0", features = ["derive"] } 12 | tao = "0.34" 13 | 14 | [[bin]] 15 | name = "bench_hello_world" 16 | path = "src/hello_world.rs" 17 | 18 | [[bin]] 19 | name = "bench_cpu_intensive" 20 | path = "src/cpu_intensive.rs" 21 | 22 | [[bin]] 23 | name = "bench_custom_protocol" 24 | path = "src/custom_protocol.rs" 25 | 26 | [profile.release] 27 | panic = "abort" 28 | codegen-units = 1 29 | lto = true 30 | incremental = false 31 | opt-level = "s" 32 | -------------------------------------------------------------------------------- /bench/Cargo.toml: -------------------------------------------------------------------------------- 1 | workspace = {} 2 | 3 | [package] 4 | name = "wry_bench" 5 | version = "0.1.0" 6 | authors = [ "Tauri Programme within The Commons Conservancy" ] 7 | edition = "2018" 8 | license = "Apache-2.0 OR MIT" 9 | description = "Cross-platform WebView rendering library" 10 | repository = "https://github.com/tauri-apps/wry" 11 | 12 | [dependencies] 13 | anyhow = "1.0" 14 | time = { version = "0.3", features = ["formatting"] } 15 | tempfile = "3.10" 16 | serde_json = "1.0" 17 | serde = { version = "1.0", features = [ "derive" ] } 18 | 19 | [[bin]] 20 | name = "run_benchmark" 21 | path = "src/run_benchmark.rs" 22 | 23 | [[bin]] 24 | name = "build_benchmark_jsons" 25 | path = "src/build_benchmark_jsons.rs" 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Tauri 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | > 1.0 | :white_check_mark: | 8 | | < 1.0 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | If you have found a potential security threat, vulnerability or exploit in Tauri 13 | or one of its upstream dependencies, please DON’T create a pull-request, DON’T 14 | file an issue on GitHub, DON’T mention it on Discord and DON’T create a forum thread. 15 | 16 | We will be adding contact information to this page very soon. 17 | 18 | At the current time we do not have the financial ability to reward bounties, 19 | but in extreme cases will at our discretion consider a reward. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve wry 4 | title: '' 5 | labels: 'type: bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Steps To Reproduce** 14 | Steps to reproduce the behavior. It **must** use wry directly instead of tauri. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Platform and Versions (please complete the following information):** 23 | OS: 24 | Rustc: 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: audit 6 | 7 | on: 8 | workflow_dispatch: 9 | schedule: 10 | - cron: '0 0 * * *' 11 | push: 12 | branches: 13 | - dev 14 | paths: 15 | - 'Cargo.lock' 16 | - 'Cargo.toml' 17 | pull_request: 18 | paths: 19 | - 'Cargo.lock' 20 | - 'Cargo.toml' 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | audit: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: rustsec/audit-check@v1 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /LICENSE.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.1 2 | DataLicense: CC0-1.0 3 | PackageName: wry 4 | DataFormat: SPDXRef-1 5 | PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy 6 | PackageHomePage: https://tauri.app 7 | PackageLicenseDeclared: Apache-2.0 8 | PackageLicenseDeclared: MIT 9 | PackageCopyrightText: 2020-2023, The Tauri Programme in the Commons Conservancy 10 | PackageSummary: Wry is the official, rust-based webview 11 | windowing service for Tauri. 12 | 13 | PackageComment: The package includes the following libraries; see 14 | Relationship information. 15 | 16 | Created: 2020-05-20T09:00:00Z 17 | PackageDownloadLocation: git://github.com/tauri-apps/wry 18 | PackageDownloadLocation: git+https://github.com/tauri-apps/wry.git 19 | PackageDownloadLocation: git+ssh://github.com/tauri-apps/wry.git 20 | Creator: Person: Daniel Thompson-Yvetot -------------------------------------------------------------------------------- /examples/custom_protocol/index.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Welcome to WRY!

15 |

Page 1

16 | 17 |

18 | Link 19 | 20 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/custom_protocol/script.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | if (window.location.pathname.startsWith("/page2")) { 5 | console.log("hello from javascript in page2"); 6 | } else { 7 | console.log("hello from javascript in page1"); 8 | 9 | if (typeof WebAssembly.instantiateStreaming !== "undefined") { 10 | WebAssembly.instantiateStreaming(fetch("/wasm.wasm")).then((wasm) => { 11 | console.log(wasm.instance.exports.main()); // should log 42 12 | }); 13 | } else { 14 | // Older WKWebView may not support `WebAssembly.instantiateStreaming` yet. 15 | fetch("/wasm.wasm") 16 | .then((response) => response.arrayBuffer()) 17 | .then((bytes) => WebAssembly.instantiate(bytes)) 18 | .then((wasm) => { 19 | console.log(wasm.instance.exports.main()); // should log 42 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/android/kotlin/proguard-wry.pro: -------------------------------------------------------------------------------- 1 | # Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | -keep class {{package-unescaped}}.* { 6 | native ; 7 | } 8 | 9 | -keep class {{package-unescaped}}.WryActivity { 10 | public (...); 11 | 12 | void setWebView({{package-unescaped}}.RustWebView); 13 | java.lang.Class getAppClass(...); 14 | java.lang.String getVersion(); 15 | } 16 | 17 | -keep class {{package-unescaped}}.Ipc { 18 | public (...); 19 | 20 | @android.webkit.JavascriptInterface public ; 21 | } 22 | 23 | -keep class {{package-unescaped}}.RustWebView { 24 | public (...); 25 | 26 | void loadUrlMainThread(...); 27 | void loadHTMLMainThread(...); 28 | void evalScript(...); 29 | } 30 | 31 | -keep class {{package-unescaped}}.RustWebChromeClient,{{package-unescaped}}.RustWebViewClient { 32 | public (...); 33 | } 34 | -------------------------------------------------------------------------------- /src/android/kotlin/Ipc.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | @file:Suppress("unused") 6 | 7 | package {{package}} 8 | 9 | import android.webkit.* 10 | 11 | class Ipc(val webViewClient: RustWebViewClient) { 12 | @JavascriptInterface 13 | fun postMessage(message: String?) { 14 | message?.let {m -> 15 | // we're not using WebView::getUrl() here because it needs to be executed on the main thread 16 | // and it would slow down the Ipc 17 | // so instead we track the current URL on the webview client 18 | this.ipc(webViewClient.currentUrl, m) 19 | } 20 | } 21 | 22 | companion object { 23 | init { 24 | System.loadLibrary("{{library}}") 25 | } 26 | } 27 | 28 | private external fun ipc(url: String, message: String) 29 | 30 | {{class-extension}} 31 | } 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /.github/workflows/covector-comment-on-fork.yml: -------------------------------------------------------------------------------- 1 | name: covector comment 2 | on: 3 | workflow_run: 4 | workflows: [covector status] # the `name` of the workflow run on `pull_request` running `status` with `comment: true` 5 | types: 6 | - completed 7 | 8 | # note all other permissions are set to none if not specified 9 | # and these set the permissions for `secrets.GITHUB_TOKEN` 10 | permissions: 11 | # to read the action artifacts on `covector status` workflows 12 | actions: read 13 | # to write the comment 14 | pull-requests: write 15 | 16 | jobs: 17 | download: 18 | runs-on: ubuntu-latest 19 | if: github.event.workflow_run.conclusion == 'success' && 20 | (github.event.workflow_run.head_repository.full_name != github.repository || github.actor == 'dependabot[bot]') 21 | steps: 22 | - name: covector status 23 | uses: jbolda/covector/packages/action@covector-v0 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | command: "status" 27 | -------------------------------------------------------------------------------- /bench/tests/src/static/worker.js: -------------------------------------------------------------------------------- 1 | const isPrime = (number) => { 2 | if (number % 2 === 0 && number > 2) { 3 | return false; 4 | } 5 | 6 | let start = 2; 7 | const limit = Math.sqrt(number); 8 | while (start <= limit) { 9 | if (number % start++ < 1) { 10 | return false; 11 | } 12 | } 13 | return number > 1; 14 | }; 15 | 16 | addEventListener("message", (e) => { 17 | const { startTime } = e.data; 18 | 19 | let n = 0; 20 | let total = 0; 21 | const THRESHOLD = e.data.value; 22 | const primes = []; 23 | 24 | let previous = startTime; 25 | 26 | while (++n <= THRESHOLD) { 27 | if (isPrime(n)) { 28 | primes.push(n); 29 | total++; 30 | 31 | const now = Date.now(); 32 | 33 | if (now - previous > 250) { 34 | previous = now; 35 | postMessage({ 36 | status: "calculating", 37 | count: total, 38 | time: Date.now() - startTime, 39 | }); 40 | } 41 | } 42 | } 43 | 44 | postMessage({ status: "done", count: total, time: Date.now() - startTime }); 45 | }); 46 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Ngo Iok Ui & Tauri Programme within The Commons Conservancy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/clippy-fmt.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: clippy & fmt 6 | 7 | on: 8 | push: 9 | branches: 10 | - dev 11 | pull_request: 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | clippy: 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | platform: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | runs-on: ${{ matrix.platform }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: install system deps 29 | if: matrix.platform == 'ubuntu-latest' 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install -y libwebkit2gtk-4.1-dev 33 | 34 | - uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: clippy 37 | 38 | - run: cargo clippy --all-targets 39 | 40 | fmt: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: dtolnay/rust-toolchain@stable 45 | with: 46 | components: rustfmt 47 | 48 | - run: cargo fmt --all -- --check 49 | -------------------------------------------------------------------------------- /examples/streaming/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |

Enter a path to a video to play, then hit Enter or click Start

10 |
11 | 12 | 13 |
14 | 15 | 16 | 17 | 38 | 39 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /bench/tests/src/static/site.js: -------------------------------------------------------------------------------- 1 | // Create web worker 2 | const THRESHOLD = 10000000; 3 | const worker = new Worker("/worker.js"); 4 | /** @type {HTMLButtonElement} */ 5 | const start = document.getElementById("start"); 6 | /** @type {HTMLParagraphElement} */ 7 | const status = document.getElementById("status"); 8 | const results = document.getElementById("results"); 9 | 10 | const ITERATIONS = 1; 11 | 12 | let resolver; 13 | 14 | const onMessage = (message) => { 15 | // Update the UI 16 | let prefix = "[Calculating]"; 17 | 18 | if (message.data.status === "done") { 19 | // tell rust that we are done 20 | ipc.postMessage("process-complete"); 21 | } 22 | 23 | status.innerHTML = `${prefix} Found ${message.data.count} prime numbers in ${message.data.time}ms`; 24 | 25 | if (message.data.status === "done") { 26 | resolver(message.data.time); 27 | } 28 | }; 29 | 30 | worker.addEventListener("message", onMessage); 31 | 32 | const benchmark = () => { 33 | return new Promise((resolve) => { 34 | const startTime = Date.now(); 35 | resolver = resolve; 36 | worker.postMessage({ value: THRESHOLD, startTime }); 37 | }); 38 | }; 39 | 40 | const calculate = async () => { 41 | let total = 0; 42 | 43 | for (let i = 0; i < ITERATIONS; i++) { 44 | const result = await benchmark(); 45 | total += result; 46 | } 47 | 48 | const average = total / ITERATIONS; 49 | 50 | results.innerText = `Average time: ${average}ms`; 51 | }; 52 | 53 | window.addEventListener("DOMContentLoaded", calculate); 54 | -------------------------------------------------------------------------------- /src/wkwebview/proxy.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use objc2_foundation::NSObject; 6 | use std::ffi::{c_char, CString}; 7 | 8 | use crate::{proxy::ProxyEndpoint, Error}; 9 | 10 | #[allow(non_camel_case_types)] 11 | pub type nw_endpoint_t = *mut NSObject; 12 | #[allow(non_camel_case_types)] 13 | pub type nw_protocol_options_t = *mut NSObject; 14 | #[allow(non_camel_case_types)] 15 | pub type nw_proxy_config_t = *mut NSObject; 16 | 17 | #[link(name = "Network", kind = "framework")] 18 | extern "C" { 19 | fn nw_endpoint_create_host(host: *const c_char, port: *const c_char) -> nw_endpoint_t; 20 | pub fn nw_proxy_config_create_socksv5(proxy_endpoint: nw_endpoint_t) -> nw_proxy_config_t; 21 | pub fn nw_proxy_config_create_http_connect( 22 | proxy_endpoint: nw_endpoint_t, 23 | proxy_tls_options: nw_protocol_options_t, 24 | ) -> nw_proxy_config_t; 25 | } 26 | 27 | impl TryFrom for nw_endpoint_t { 28 | type Error = Error; 29 | fn try_from(endpoint: ProxyEndpoint) -> Result { 30 | unsafe { 31 | let endpoint_host = 32 | CString::new(endpoint.host).map_err(|_| Error::ProxyEndpointCreationFailed)?; 33 | let endpoint_port = 34 | CString::new(endpoint.port).map_err(|_| Error::ProxyEndpointCreationFailed)?; 35 | let endpoint = nw_endpoint_create_host(endpoint_host.as_ptr(), endpoint_port.as_ptr()); 36 | 37 | if endpoint.is_null() { 38 | Err(Error::ProxyEndpointCreationFailed) 39 | } else { 40 | Ok(endpoint) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.changes/readme.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ##### via https://github.com/jbolda/covector 4 | 5 | As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version _number_, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend that it represents the overall change for organizational purposes. 6 | 7 | When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process. 8 | 9 | Use the following format: 10 | 11 | ```md 12 | --- 13 | "wry": patch 14 | --- 15 | 16 | Change summary goes here 17 | 18 | ``` 19 | 20 | Summaries do not have a specific character limit, but are text only. These summaries are used within the (future implementation of) changelogs. They will give context to the change and also point back to the original PR if more details and context are needed. 21 | 22 | Changes will be designated as a `major`, `minor` or `patch` as further described in [semver](https://semver.org/). 23 | 24 | Given a version number MAJOR.MINOR.PATCH, increment the: 25 | 26 | - MAJOR version when you make incompatible API changes, 27 | - MINOR version when you add functionality in a backwards compatible manner, and 28 | - PATCH version when you make backwards compatible bug fixes. 29 | 30 | Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format, but will be discussed prior to usage (as extra steps will be necessary in consideration of merging and publishing). 31 | -------------------------------------------------------------------------------- /bench/src/build_benchmark_jsons.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::{fs::File, io::BufReader}; 6 | mod utils; 7 | 8 | fn main() { 9 | let wry_data = &utils::wry_root_path() 10 | .join("gh-pages") 11 | .join("wry-data.json"); 12 | let wry_recent = &utils::wry_root_path() 13 | .join("gh-pages") 14 | .join("wry-recent.json"); 15 | 16 | // current data 17 | let current_data_buffer = BufReader::new( 18 | File::open(utils::target_dir().join("bench.json")).expect("Unable to read current data file"), 19 | ); 20 | let current_data: utils::BenchResult = 21 | serde_json::from_reader(current_data_buffer).expect("Unable to read current data buffer"); 22 | 23 | // all data's 24 | let all_data_buffer = BufReader::new(File::open(wry_data).expect("Unable to read all data file")); 25 | let mut all_data: Vec = 26 | serde_json::from_reader(all_data_buffer).expect("Unable to read all data buffer"); 27 | 28 | // add current data to all data 29 | all_data.push(current_data); 30 | 31 | // use only latest 20 elements from all data 32 | let recent: Vec = if all_data.len() > 20 { 33 | all_data[all_data.len() - 20..].to_vec() 34 | } else { 35 | all_data.clone() 36 | }; 37 | 38 | // write jsons 39 | utils::write_json( 40 | wry_data.to_str().expect("Something wrong with wry_data"), 41 | &serde_json::to_value(&all_data).expect("Unable to build final json (all)"), 42 | ) 43 | .unwrap_or_else(|_| panic!("Unable to write {:?}", wry_data)); 44 | 45 | utils::write_json( 46 | wry_recent 47 | .to_str() 48 | .expect("Something wrong with wry_recent"), 49 | &serde_json::to_value(recent).expect("Unable to build final json (recent)"), 50 | ) 51 | .unwrap_or_else(|_| panic!("Unable to write {:?}", wry_recent)); 52 | } 53 | -------------------------------------------------------------------------------- /bench/tests/src/hello_world.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use std::process::exit; 7 | 8 | #[derive(Debug, Serialize, Deserialize)] 9 | struct MessageParameters { 10 | message: String, 11 | } 12 | 13 | fn main() -> wry::Result<()> { 14 | use tao::{ 15 | event::{Event, WindowEvent}, 16 | event_loop::{ControlFlow, EventLoop}, 17 | window::WindowBuilder, 18 | }; 19 | use wry::http::Request; 20 | use wry::WebViewBuilder; 21 | 22 | let event_loop = EventLoop::new(); 23 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 24 | 25 | let html = r#" 26 | 27 | 28 | 33 | 34 | "#; 35 | 36 | let handler = |req: Request| { 37 | if req.body() == "dom-loaded" { 38 | exit(0); 39 | } 40 | }; 41 | 42 | let builder = WebViewBuilder::new() 43 | .with_html(html) 44 | .with_ipc_handler(handler); 45 | 46 | #[cfg(any( 47 | target_os = "windows", 48 | target_os = "macos", 49 | target_os = "ios", 50 | target_os = "android" 51 | ))] 52 | let _webview = builder.build(&window)?; 53 | 54 | #[cfg(not(any( 55 | target_os = "windows", 56 | target_os = "macos", 57 | target_os = "ios", 58 | target_os = "android" 59 | )))] 60 | { 61 | use tao::platform::unix::WindowExtUnix; 62 | use wry::WebViewBuilderExtUnix; 63 | let vbox = window.default_vbox().unwrap(); 64 | let _webview = builder.build_gtk(vbox)?; 65 | }; 66 | 67 | event_loop.run(move |event, _, control_flow| { 68 | *control_flow = ControlFlow::Wait; 69 | 70 | match event { 71 | Event::WindowEvent { 72 | event: WindowEvent::CloseRequested, 73 | .. 74 | } => *control_flow = ControlFlow::Exit, 75 | _ => {} 76 | } 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /examples/cookies.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use tao::{ 6 | event::{Event, WindowEvent}, 7 | event_loop::{ControlFlow, EventLoop}, 8 | window::WindowBuilder, 9 | }; 10 | use wry::WebViewBuilder; 11 | 12 | fn main() -> wry::Result<()> { 13 | let event_loop = EventLoop::new(); 14 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 15 | 16 | let builder = WebViewBuilder::new().with_url("https://www.httpbin.org/cookies/set?foo=bar"); 17 | 18 | #[cfg(any( 19 | target_os = "windows", 20 | target_os = "macos", 21 | target_os = "ios", 22 | target_os = "android" 23 | ))] 24 | let webview = builder.build(&window)?; 25 | #[cfg(not(any( 26 | target_os = "windows", 27 | target_os = "macos", 28 | target_os = "ios", 29 | target_os = "android" 30 | )))] 31 | let webview = { 32 | use tao::platform::unix::WindowExtUnix; 33 | use wry::WebViewBuilderExtUnix; 34 | let vbox = window.default_vbox().unwrap(); 35 | builder.build_gtk(vbox)? 36 | }; 37 | 38 | webview.set_cookie( 39 | cookie::Cookie::build(("foo1", "bar1")) 40 | .domain("www.httpbin.org") 41 | .path("/") 42 | .secure(true) 43 | .http_only(true) 44 | .max_age(cookie::time::Duration::seconds(10)) 45 | .inner(), 46 | )?; 47 | 48 | let cookie_deleted = cookie::Cookie::build(("will_be_deleted", "will_be_deleted")); 49 | 50 | webview.set_cookie(cookie_deleted.inner())?; 51 | println!("Setting Cookies:"); 52 | for cookie in webview.cookies()? { 53 | println!("\t{cookie}"); 54 | } 55 | 56 | println!("After Deleting:"); 57 | webview.delete_cookie(cookie_deleted.inner())?; 58 | for cookie in webview.cookies()? { 59 | println!("\t{cookie}"); 60 | } 61 | 62 | event_loop.run(move |event, _, control_flow| { 63 | *control_flow = ControlFlow::Wait; 64 | 65 | if let Event::WindowEvent { 66 | event: WindowEvent::CloseRequested, 67 | .. 68 | } = event 69 | { 70 | *control_flow = ControlFlow::Exit; 71 | } 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use tao::{ 6 | event::{Event, WindowEvent}, 7 | event_loop::{ControlFlow, EventLoop}, 8 | window::WindowBuilder, 9 | }; 10 | use wry::WebViewBuilder; 11 | 12 | fn main() -> wry::Result<()> { 13 | let event_loop = EventLoop::new(); 14 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 15 | 16 | let builder = WebViewBuilder::new() 17 | .with_url("http://tauri.app") 18 | .with_new_window_req_handler(|url, features| { 19 | println!("new window req: {url} {features:?}"); 20 | wry::NewWindowResponse::Allow 21 | }); 22 | 23 | #[cfg(feature = "drag-drop")] 24 | let builder = builder.with_drag_drop_handler(|e| { 25 | match e { 26 | wry::DragDropEvent::Enter { paths, position } => { 27 | println!("DragEnter: {position:?} {paths:?} ") 28 | } 29 | wry::DragDropEvent::Over { position } => println!("DragOver: {position:?} "), 30 | wry::DragDropEvent::Drop { paths, position } => { 31 | println!("DragDrop: {position:?} {paths:?} ") 32 | } 33 | wry::DragDropEvent::Leave => println!("DragLeave"), 34 | _ => {} 35 | } 36 | 37 | true 38 | }); 39 | 40 | #[cfg(any( 41 | target_os = "windows", 42 | target_os = "macos", 43 | target_os = "ios", 44 | target_os = "android" 45 | ))] 46 | let _webview = builder.build(&window)?; 47 | #[cfg(not(any( 48 | target_os = "windows", 49 | target_os = "macos", 50 | target_os = "ios", 51 | target_os = "android" 52 | )))] 53 | let _webview = { 54 | use tao::platform::unix::WindowExtUnix; 55 | use wry::WebViewBuilderExtUnix; 56 | let vbox = window.default_vbox().unwrap(); 57 | builder.build_gtk(vbox)? 58 | }; 59 | 60 | event_loop.run(move |event, _, control_flow| { 61 | *control_flow = ControlFlow::Wait; 62 | 63 | if let Event::WindowEvent { 64 | event: WindowEvent::CloseRequested, 65 | .. 66 | } = event 67 | { 68 | *control_flow = ControlFlow::Exit; 69 | } 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /wry-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.changes/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitSiteUrl": "https://github.com/tauri-apps/wry/", 3 | "timeout": 3600000, 4 | "additionalBumpTypes": ["housekeeping"], 5 | "pkgManagers": { 6 | "rust": { 7 | "version": true, 8 | "getPublishedVersion": { 9 | "use": "fetch:check", 10 | "options": { 11 | "url": "https://crates.io/api/v1/crates/${ pkg.pkg }/${ pkg.pkgFile.version }" 12 | } 13 | }, 14 | "prepublish": [ 15 | { 16 | "command": "cargo install cargo-audit --features=fix", 17 | "dryRunCommand": true 18 | }, 19 | { 20 | "command": "echo '
\n

Cargo Audit

\n\n```'", 21 | "dryRunCommand": true, 22 | "pipe": true 23 | }, 24 | { 25 | "command": "cargo generate-lockfile", 26 | "dryRunCommand": true, 27 | "runFromRoot": true, 28 | "pipe": true 29 | }, 30 | { 31 | "command": "cargo audit ${ process.env.CARGO_AUDIT_OPTIONS || '' }", 32 | "dryRunCommand": true, 33 | "runFromRoot": true, 34 | "pipe": true 35 | }, 36 | { 37 | "command": "echo '```\n\n
\n'", 38 | "dryRunCommand": true, 39 | "pipe": true 40 | } 41 | ], 42 | "publish": [ 43 | { 44 | "command": "echo '
\n

Cargo Publish

\n\n```'", 45 | "dryRunCommand": true, 46 | "pipe": true 47 | }, 48 | { 49 | "command": "cargo publish --no-verify --allow-dirty", 50 | "dryRunCommand": "cargo publish --no-verify --allow-dirty --dry-run", 51 | "pipe": true 52 | }, 53 | { 54 | "command": "echo '```\n\n
\n'", 55 | "dryRunCommand": true, 56 | "pipe": true 57 | } 58 | ], 59 | "postpublish": [ 60 | "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor } -f", 61 | "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor }.${ pkgFile.versionMinor } -f", 62 | "git push --tags -f" 63 | ] 64 | } 65 | }, 66 | "packages": { 67 | "wry": { 68 | "path": "./", 69 | "manager": "rust" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/android/kotlin/Logger.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | @file:Suppress("unused", "MemberVisibilityCanBePrivate") 6 | 7 | package {{package}} 8 | 9 | // taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java 10 | 11 | import android.text.TextUtils 12 | import android.util.Log 13 | 14 | class Logger { 15 | companion object { 16 | private const val LOG_TAG_CORE = "Tauri" 17 | 18 | fun tags(vararg subtags: String): String { 19 | return if (subtags.isNotEmpty()) { 20 | LOG_TAG_CORE + "/" + TextUtils.join("/", subtags) 21 | } else LOG_TAG_CORE 22 | } 23 | 24 | fun verbose(message: String) { 25 | verbose(LOG_TAG_CORE, message) 26 | } 27 | 28 | private fun verbose(tag: String, message: String) { 29 | if (!shouldLog()) { 30 | return 31 | } 32 | Log.v(tag, message) 33 | } 34 | 35 | fun debug(message: String) { 36 | debug(LOG_TAG_CORE, message) 37 | } 38 | 39 | fun debug(tag: String, message: String) { 40 | if (!shouldLog()) { 41 | return 42 | } 43 | Log.d(tag, message) 44 | } 45 | 46 | fun info(message: String) { 47 | info(LOG_TAG_CORE, message) 48 | } 49 | 50 | fun info(tag: String, message: String) { 51 | if (!shouldLog()) { 52 | return 53 | } 54 | Log.i(tag, message) 55 | } 56 | 57 | fun warn(message: String) { 58 | warn(LOG_TAG_CORE, message) 59 | } 60 | 61 | fun warn(tag: String, message: String) { 62 | if (!shouldLog()) { 63 | return 64 | } 65 | Log.w(tag, message) 66 | } 67 | 68 | fun error(message: String) { 69 | error(LOG_TAG_CORE, message, null) 70 | } 71 | 72 | fun error(message: String, e: Throwable?) { 73 | error(LOG_TAG_CORE, message, e) 74 | } 75 | 76 | fun error(tag: String, message: String, e: Throwable?) { 77 | if (!shouldLog()) { 78 | return 79 | } 80 | Log.e(tag, message, e) 81 | } 82 | 83 | private fun shouldLog(): Boolean { 84 | return BuildConfig.DEBUG 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/covector-version-or-publish.yml: -------------------------------------------------------------------------------- 1 | name: covector version or publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | 8 | jobs: 9 | version-or-publish: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 65 12 | outputs: 13 | change: ${{ steps.covector.outputs.change }} 14 | commandRan: ${{ steps.covector.outputs.commandRan }} 15 | successfulPublish: ${{ steps.covector.outputs.successfulPublish }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 # required for use of git history 21 | 22 | - name: cargo login 23 | run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }} 24 | 25 | - name: git config 26 | run: | 27 | git config --global user.name "${{ github.event.pusher.name }}" 28 | git config --global user.email "${{ github.event.pusher.email }}" 29 | 30 | - name: covector version or publish (publish when no change files present) 31 | uses: jbolda/covector/packages/action@covector-v0 32 | id: covector 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | CARGO_AUDIT_OPTIONS: ${{ secrets.CARGO_AUDIT_OPTIONS }} 36 | with: 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | command: "version-or-publish" 39 | createRelease: true 40 | recognizeContributors: true 41 | 42 | - name: install cargo-readme 43 | if: steps.covector.outputs.commandRan == 'version' 44 | run: cargo install cargo-readme --locked 45 | 46 | - run: cargo readme --no-title --no-license > README.md 47 | if: steps.covector.outputs.commandRan == 'version' 48 | 49 | - name: Sync Cargo.lock 50 | if: steps.covector.outputs.commandRan == 'version' 51 | run: cargo tree --depth 0 52 | 53 | - name: Create Pull Request With Versions Bumped 54 | id: cpr 55 | uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # 7.0.7 56 | if: steps.covector.outputs.commandRan == 'version' 57 | with: 58 | token: ${{ secrets.GITHUB_TOKEN }} 59 | title: Apply Version Updates From Current Changes 60 | commit-message: "apply version updates" 61 | labels: "version updates" 62 | branch: "ci/pending-release" 63 | body: ${{ steps.covector.outputs.change }} 64 | sign-commits: true 65 | -------------------------------------------------------------------------------- /examples/transparent.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use tao::{ 6 | event::{Event, WindowEvent}, 7 | event_loop::{ControlFlow, EventLoop}, 8 | window::WindowBuilder, 9 | }; 10 | use wry::WebViewBuilder; 11 | 12 | fn main() -> wry::Result<()> { 13 | let event_loop = EventLoop::new(); 14 | #[allow(unused_mut)] 15 | let mut builder = WindowBuilder::new() 16 | .with_decorations(false) 17 | // There are actually three layer of background color when creating webview window. 18 | // The first is window background... 19 | .with_transparent(true); 20 | #[cfg(target_os = "windows")] 21 | { 22 | use tao::platform::windows::WindowBuilderExtWindows; 23 | builder = builder.with_undecorated_shadow(false); 24 | } 25 | let window = builder.build(&event_loop).unwrap(); 26 | 27 | #[cfg(target_os = "windows")] 28 | { 29 | use tao::platform::windows::WindowExtWindows; 30 | window.set_undecorated_shadow(true); 31 | } 32 | 33 | let builder = WebViewBuilder::new() 34 | // The second is on webview... 35 | // Feature `transparent` is required for transparency to work. 36 | .with_transparent(true) 37 | // And the last is in html. 38 | .with_html( 39 | r#" 40 | 41 | 46 | "#, 47 | ); 48 | 49 | #[cfg(any( 50 | target_os = "windows", 51 | target_os = "macos", 52 | target_os = "ios", 53 | target_os = "android" 54 | ))] 55 | let _webview = builder.build(&window)?; 56 | #[cfg(not(any( 57 | target_os = "windows", 58 | target_os = "macos", 59 | target_os = "ios", 60 | target_os = "android" 61 | )))] 62 | let _webview = { 63 | use tao::platform::unix::WindowExtUnix; 64 | use wry::WebViewBuilderExtUnix; 65 | let vbox = window.default_vbox().unwrap(); 66 | builder.build_gtk(vbox)? 67 | }; 68 | 69 | event_loop.run(move |event, _, control_flow| { 70 | *control_flow = ControlFlow::Wait; 71 | 72 | if let Event::WindowEvent { 73 | event: WindowEvent::CloseRequested, 74 | .. 75 | } = event 76 | { 77 | *control_flow = ControlFlow::Exit 78 | } 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /bench/tests/src/custom_protocol.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use std::process::exit; 7 | 8 | const INDEX_HTML: &[u8] = br#" 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

Welcome to WRY!

18 | 23 | 24 | "#; 25 | 26 | #[derive(Debug, Serialize, Deserialize)] 27 | struct MessageParameters { 28 | message: String, 29 | } 30 | 31 | fn main() -> wry::Result<()> { 32 | use tao::{ 33 | event::{Event, WindowEvent}, 34 | event_loop::{ControlFlow, EventLoop}, 35 | window::WindowBuilder, 36 | }; 37 | use wry::http::Request; 38 | use wry::{ 39 | http::{header::CONTENT_TYPE, Response}, 40 | WebViewBuilder, 41 | }; 42 | 43 | let event_loop = EventLoop::new(); 44 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 45 | 46 | let handler = |req: Request| { 47 | if req.body() == "dom-loaded" { 48 | exit(0); 49 | } 50 | }; 51 | 52 | let builder = WebViewBuilder::new() 53 | .with_ipc_handler(handler) 54 | .with_custom_protocol("wrybench".into(), move |_id, _request| { 55 | Response::builder() 56 | .header(CONTENT_TYPE, "text/html") 57 | .body(INDEX_HTML.into()) 58 | .unwrap() 59 | }) 60 | .with_url("wrybench://localhost"); 61 | 62 | #[cfg(any( 63 | target_os = "windows", 64 | target_os = "macos", 65 | target_os = "ios", 66 | target_os = "android" 67 | ))] 68 | let _webview = builder.build(&window)?; 69 | 70 | #[cfg(not(any( 71 | target_os = "windows", 72 | target_os = "macos", 73 | target_os = "ios", 74 | target_os = "android" 75 | )))] 76 | { 77 | use tao::platform::unix::WindowExtUnix; 78 | use wry::WebViewBuilderExtUnix; 79 | let vbox = window.default_vbox().unwrap(); 80 | let _webview = builder.build_gtk(vbox)?; 81 | }; 82 | 83 | event_loop.run(move |event, _, control_flow| { 84 | *control_flow = ControlFlow::Wait; 85 | 86 | match event { 87 | Event::WindowEvent { 88 | event: WindowEvent::CloseRequested, 89 | .. 90 | } => *control_flow = ControlFlow::Exit, 91 | _ => {} 92 | } 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/wkwebview/class/wry_download_delegate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::{cell::RefCell, path::PathBuf, rc::Rc}; 6 | 7 | use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly}; 8 | use objc2_foundation::{ 9 | MainThreadMarker, NSData, NSError, NSObjectProtocol, NSString, NSURLResponse, NSURL, 10 | }; 11 | use objc2_web_kit::{WKDownload, WKDownloadDelegate}; 12 | 13 | use crate::wkwebview::download::{download_did_fail, download_did_finish, download_policy}; 14 | 15 | pub struct WryDownloadDelegateIvars { 16 | pub started: Option bool + 'static>>>, 17 | pub completed: Option, bool) + 'static>>, 18 | } 19 | 20 | define_class!( 21 | #[unsafe(super(NSObject))] 22 | #[name = "WryDownloadDelegate"] 23 | #[thread_kind = MainThreadOnly] 24 | #[ivars = WryDownloadDelegateIvars] 25 | pub struct WryDownloadDelegate; 26 | 27 | unsafe impl NSObjectProtocol for WryDownloadDelegate {} 28 | 29 | unsafe impl WKDownloadDelegate for WryDownloadDelegate { 30 | #[unsafe(method(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:))] 31 | fn download_policy( 32 | &self, 33 | download: &WKDownload, 34 | response: &NSURLResponse, 35 | suggested_filename: &NSString, 36 | handler: &block2::Block, 37 | ) { 38 | download_policy(self, download, response, suggested_filename, handler); 39 | } 40 | 41 | #[unsafe(method(downloadDidFinish:))] 42 | fn download_did_finish(&self, download: &WKDownload) { 43 | download_did_finish(self, download); 44 | } 45 | 46 | #[unsafe(method(download:didFailWithError:resumeData:))] 47 | fn download_did_fail(&self, download: &WKDownload, error: &NSError, resume_data: &NSData) { 48 | download_did_fail(self, download, error, resume_data); 49 | } 50 | } 51 | ); 52 | 53 | impl WryDownloadDelegate { 54 | pub fn new( 55 | download_started_handler: Option bool + 'static>>, 56 | download_completed_handler: Option, bool) + 'static>>, 57 | mtm: MainThreadMarker, 58 | ) -> Retained { 59 | let delegate = mtm 60 | .alloc::() 61 | .set_ivars(WryDownloadDelegateIvars { 62 | started: download_started_handler.map(|handler| RefCell::new(handler)), 63 | completed: download_completed_handler, 64 | }); 65 | 66 | unsafe { msg_send![super(delegate), init] } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /bench/tests/src/cpu_intensive.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::process::exit; 6 | 7 | fn main() -> wry::Result<()> { 8 | use tao::{ 9 | event::{Event, WindowEvent}, 10 | event_loop::{ControlFlow, EventLoop}, 11 | window::WindowBuilder, 12 | }; 13 | use wry::http::Request; 14 | use wry::{ 15 | http::{header::CONTENT_TYPE, Response}, 16 | WebViewBuilder, 17 | }; 18 | 19 | let event_loop = EventLoop::new(); 20 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 21 | 22 | let handler = |req: Request| { 23 | if req.body() == "process-complete" { 24 | exit(0); 25 | } 26 | }; 27 | 28 | let builder = WebViewBuilder::new() 29 | .with_custom_protocol("wrybench".into(), move |_id, request| { 30 | let path = request.uri().to_string(); 31 | let requested_asset_path = path.strip_prefix("wrybench://localhost").unwrap(); 32 | let (data, mimetype): (_, String) = match requested_asset_path { 33 | "/index.css" => ( 34 | include_bytes!("static/index.css").as_slice().into(), 35 | "text/css".into(), 36 | ), 37 | "/site.js" => ( 38 | include_bytes!("static/site.js").as_slice().into(), 39 | "text/javascript".into(), 40 | ), 41 | "/worker.js" => ( 42 | include_bytes!("static/worker.js").as_slice().into(), 43 | "text/javascript".into(), 44 | ), 45 | _ => ( 46 | include_bytes!("static/index.html").as_slice().into(), 47 | "text/html".into(), 48 | ), 49 | }; 50 | 51 | Response::builder() 52 | .header(CONTENT_TYPE, mimetype) 53 | .body(data) 54 | .unwrap() 55 | }) 56 | .with_url("wrybench://localhost") 57 | .with_ipc_handler(handler); 58 | 59 | #[cfg(any( 60 | target_os = "windows", 61 | target_os = "macos", 62 | target_os = "ios", 63 | target_os = "android" 64 | ))] 65 | let _webview = builder.build(&window)?; 66 | 67 | #[cfg(not(any( 68 | target_os = "windows", 69 | target_os = "macos", 70 | target_os = "ios", 71 | target_os = "android" 72 | )))] 73 | { 74 | use tao::platform::unix::WindowExtUnix; 75 | use wry::WebViewBuilderExtUnix; 76 | let vbox = window.default_vbox().unwrap(); 77 | let _webview = builder.build_gtk(vbox)?; 78 | } 79 | 80 | event_loop.run(move |event, _, control_flow| { 81 | *control_flow = ControlFlow::Wait; 82 | 83 | match event { 84 | Event::WindowEvent { 85 | event: WindowEvent::CloseRequested, 86 | .. 87 | } => *control_flow = ControlFlow::Exit, 88 | _ => {} 89 | } 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/wkwebview/class/document_title_changed_observer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::{ffi::c_void, ptr::null_mut}; 6 | 7 | use objc2::{ 8 | define_class, msg_send, 9 | rc::Retained, 10 | runtime::{AnyObject, NSObject}, 11 | AllocAnyThread, DefinedClass, 12 | }; 13 | use objc2_foundation::{ 14 | NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions, 15 | NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSString, 16 | }; 17 | 18 | use crate::WryWebView; 19 | pub struct DocumentTitleChangedObserverIvars { 20 | pub object: Retained, 21 | pub handler: Box, 22 | } 23 | 24 | define_class!( 25 | #[unsafe(super(NSObject))] 26 | #[name = "DocumentTitleChangedObserver"] 27 | #[ivars = DocumentTitleChangedObserverIvars] 28 | pub struct DocumentTitleChangedObserver; 29 | 30 | /// NSKeyValueObserving. 31 | impl DocumentTitleChangedObserver { 32 | #[unsafe(method(observeValueForKeyPath:ofObject:change:context:))] 33 | fn observe_value_for_key_path( 34 | &self, 35 | key_path: Option<&NSString>, 36 | of_object: Option<&AnyObject>, 37 | _change: Option<&NSDictionary>, 38 | _context: *mut c_void, 39 | ) { 40 | if let (Some(key_path), Some(object)) = (key_path, of_object) { 41 | if key_path.to_string() == "title" { 42 | unsafe { 43 | let handler = &self.ivars().handler; 44 | // if !handler.is_null() { 45 | let title: *const NSString = msg_send![object, title]; 46 | handler((*title).to_string()); 47 | // } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | unsafe impl NSObjectProtocol for DocumentTitleChangedObserver {} 55 | ); 56 | 57 | impl DocumentTitleChangedObserver { 58 | pub fn new(webview: Retained, handler: Box) -> Retained { 59 | let observer = Self::alloc().set_ivars(DocumentTitleChangedObserverIvars { 60 | object: webview, 61 | handler, 62 | }); 63 | 64 | let observer: Retained = unsafe { msg_send![super(observer), init] }; 65 | 66 | unsafe { 67 | observer 68 | .ivars() 69 | .object 70 | .addObserver_forKeyPath_options_context( 71 | &observer, 72 | &NSString::from_str("title"), 73 | NSKeyValueObservingOptions::New, 74 | null_mut(), 75 | ); 76 | } 77 | 78 | observer 79 | } 80 | } 81 | 82 | impl Drop for DocumentTitleChangedObserver { 83 | fn drop(&mut self) { 84 | unsafe { 85 | self 86 | .ivars() 87 | .object 88 | .removeObserver_forKeyPath(self, &NSString::from_str("title")); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /examples/winit.rs: -------------------------------------------------------------------------------- 1 | use dpi::{LogicalPosition, LogicalSize}; 2 | use winit::{ 3 | application::ApplicationHandler, 4 | event::WindowEvent, 5 | event_loop::{ActiveEventLoop, EventLoop}, 6 | window::{Window, WindowId}, 7 | }; 8 | use wry::{Rect, WebViewBuilder}; 9 | 10 | #[derive(Default)] 11 | struct State { 12 | window: Option, 13 | webview: Option, 14 | } 15 | 16 | impl ApplicationHandler for State { 17 | fn resumed(&mut self, event_loop: &ActiveEventLoop) { 18 | let mut attributes = Window::default_attributes(); 19 | attributes.inner_size = Some(LogicalSize::new(800, 800).into()); 20 | let window = event_loop.create_window(attributes).unwrap(); 21 | 22 | let webview = WebViewBuilder::new() 23 | .with_url("https://tauri.app") 24 | .build_as_child(&window) 25 | .unwrap(); 26 | 27 | self.window = Some(window); 28 | self.webview = Some(webview); 29 | } 30 | 31 | fn window_event( 32 | &mut self, 33 | _event_loop: &ActiveEventLoop, 34 | _window_id: WindowId, 35 | event: WindowEvent, 36 | ) { 37 | match event { 38 | WindowEvent::Resized(size) => { 39 | let window = self.window.as_ref().unwrap(); 40 | let webview = self.webview.as_ref().unwrap(); 41 | 42 | let size = size.to_logical::(window.scale_factor()); 43 | webview 44 | .set_bounds(Rect { 45 | position: LogicalPosition::new(0, 0).into(), 46 | size: LogicalSize::new(size.width, size.height).into(), 47 | }) 48 | .unwrap(); 49 | } 50 | WindowEvent::CloseRequested => { 51 | std::process::exit(0); 52 | } 53 | _ => {} 54 | } 55 | } 56 | 57 | fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { 58 | #[cfg(any( 59 | target_os = "linux", 60 | target_os = "dragonfly", 61 | target_os = "freebsd", 62 | target_os = "netbsd", 63 | target_os = "openbsd", 64 | ))] 65 | { 66 | while gtk::events_pending() { 67 | gtk::main_iteration_do(false); 68 | } 69 | } 70 | } 71 | } 72 | 73 | fn main() -> wry::Result<()> { 74 | #[cfg(any( 75 | target_os = "linux", 76 | target_os = "dragonfly", 77 | target_os = "freebsd", 78 | target_os = "netbsd", 79 | target_os = "openbsd", 80 | ))] 81 | { 82 | use gtk::prelude::DisplayExtManual; 83 | 84 | gtk::init().unwrap(); 85 | if gtk::gdk::Display::default().unwrap().backend().is_wayland() { 86 | panic!("This example doesn't support wayland!"); 87 | } 88 | 89 | winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| { 90 | let error = error as *mut x11_dl::xlib::XErrorEvent; 91 | (unsafe { (*error).error_code }) == 170 92 | })); 93 | } 94 | 95 | let event_loop = EventLoop::new().unwrap(); 96 | let mut state = State::default(); 97 | event_loop.run_app(&mut state).unwrap(); 98 | 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /// Convenient type alias of Result type for wry. 2 | pub type Result = std::result::Result; 3 | 4 | /// Errors returned by wry. 5 | #[non_exhaustive] 6 | #[derive(thiserror::Error, Debug)] 7 | pub enum Error { 8 | #[cfg(gtk)] 9 | #[error(transparent)] 10 | GlibError(#[from] gtk::glib::Error), 11 | #[cfg(gtk)] 12 | #[error(transparent)] 13 | GlibBoolError(#[from] gtk::glib::BoolError), 14 | #[cfg(gtk)] 15 | #[error("Fail to fetch security manager")] 16 | MissingManager, 17 | #[cfg(gtk)] 18 | #[error("Couldn't find X11 Display")] 19 | X11DisplayNotFound, 20 | #[cfg(all(gtk, feature = "x11"))] 21 | #[error(transparent)] 22 | XlibError(#[from] x11_dl::error::OpenError), 23 | #[error("Failed to initialize the script")] 24 | InitScriptError, 25 | #[error("Bad RPC request: {0} ((1))")] 26 | RpcScriptError(String, String), 27 | #[error(transparent)] 28 | NulError(#[from] std::ffi::NulError), 29 | #[error(transparent)] 30 | ReceiverError(#[from] std::sync::mpsc::RecvError), 31 | #[cfg(target_os = "android")] 32 | #[error(transparent)] 33 | ReceiverTimeoutError(#[from] crossbeam_channel::RecvTimeoutError), 34 | #[error(transparent)] 35 | SenderError(#[from] std::sync::mpsc::SendError), 36 | #[error("Failed to send the message")] 37 | MessageSender, 38 | #[error("IO error: {0}")] 39 | Io(#[from] std::io::Error), 40 | #[cfg(target_os = "windows")] 41 | #[error("WebView2 error: {0}")] 42 | WebView2Error(webview2_com::Error), 43 | #[error(transparent)] 44 | HttpError(#[from] http::Error), 45 | #[error("Infallible error, something went really wrong: {0}")] 46 | Infallible(#[from] std::convert::Infallible), 47 | #[cfg(target_os = "android")] 48 | #[error(transparent)] 49 | JniError(#[from] jni::errors::Error), 50 | #[error("Failed to create proxy endpoint")] 51 | ProxyEndpointCreationFailed, 52 | #[error(transparent)] 53 | WindowHandleError(#[from] raw_window_handle::HandleError), 54 | #[error("the window handle kind is not supported")] 55 | UnsupportedWindowHandle, 56 | #[error(transparent)] 57 | Utf8Error(#[from] std::str::Utf8Error), 58 | #[cfg(target_os = "android")] 59 | #[error(transparent)] 60 | CrossBeamRecvError(#[from] crossbeam_channel::RecvError), 61 | #[error("not on the main thread")] 62 | NotMainThread, 63 | #[error("Custom protocol task is invalid.")] 64 | CustomProtocolTaskInvalid, 65 | #[error("Failed to register URL scheme: {0}, could be due to invalid URL scheme or the scheme is already registered.")] 66 | UrlSchemeRegisterError(String), 67 | #[error("Duplicate custom protocol '{0}' registered on the WebViewBuilder")] 68 | DuplicateCustomProtocol(String), 69 | #[error("Duplicate custom protocol '{0}' registered on the same web context on Linux")] 70 | ContextDuplicateCustomProtocol(String), 71 | #[error(transparent)] 72 | #[cfg(any(target_os = "macos", target_os = "ios"))] 73 | UrlParse(#[from] url::ParseError), 74 | #[cfg(any(target_os = "macos", target_os = "ios"))] 75 | #[error("data store is currently opened")] 76 | DataStoreInUse, 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: benches 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | workflow_dispatch: 8 | 9 | env: 10 | RUST_BACKTRACE: 1 11 | CARGO_PROFILE_DEV_DEBUG: 0 # This would add unnecessary bloat to the target folder, decreasing cache efficiency. 12 | LC_ALL: en_US.UTF-8 # This prevents strace from changing its number format to use commas. 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | bench: 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | rust: [nightly] 24 | platform: 25 | - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest } 26 | 27 | runs-on: ${{ matrix.platform.os }} 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: install Rust ${{ matrix.rust }} 33 | uses: dtolnay/rust-toolchain@master 34 | with: 35 | toolchain: ${{ matrix.rust }} 36 | components: rust-src 37 | targets: ${{ matrix.platform.target }} 38 | 39 | - name: Setup python 40 | uses: actions/setup-python@v4 41 | with: 42 | python-version: '3.10' 43 | architecture: x64 44 | 45 | - name: install dependencies 46 | run: | 47 | python -m pip install --upgrade pip 48 | sudo apt-get update 49 | sudo apt-get install -y --no-install-recommends \ 50 | libwebkit2gtk-4.1-dev libayatana-appindicator3-dev \ 51 | xvfb \ 52 | at-spi2-core 53 | wget https://github.com/sharkdp/hyperfine/releases/download/v1.18.0/hyperfine_1.18.0_amd64.deb 54 | sudo dpkg -i hyperfine_1.18.0_amd64.deb 55 | pip install memory_profiler 56 | 57 | - uses: Swatinem/rust-cache@v2 58 | with: 59 | workspaces: | 60 | . 61 | bench/tests 62 | 63 | - name: run benchmarks 64 | run: | 65 | cargo +nightly build --release -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target ${{ matrix.platform.target }} --manifest-path bench/tests/Cargo.toml 66 | xvfb-run --auto-servernum cargo run --manifest-path ./bench/Cargo.toml --bin run_benchmark 67 | 68 | - name: clone benchmarks_results 69 | if: github.repository == 'tauri-apps/wry' && github.ref == 'refs/heads/dev' 70 | uses: actions/checkout@v4 71 | with: 72 | token: ${{ secrets.BENCH_PAT }} 73 | path: gh-pages 74 | repository: tauri-apps/benchmark_results 75 | 76 | - name: push new benchmarks 77 | if: github.repository == 'tauri-apps/wry' && github.ref == 'refs/heads/dev' 78 | run: | 79 | cargo run --manifest-path ./bench/Cargo.toml --bin build_benchmark_jsons 80 | cd gh-pages 81 | git pull 82 | git config user.name "tauri-bench" 83 | git config user.email "gh.tauribot@gmail.com" 84 | git add . 85 | git commit --message "Update WRY benchmarks" 86 | git push origin gh-pages 87 | 88 | - name: Print worker info 89 | run: | 90 | cat /proc/cpuinfo 91 | cat /proc/meminfo 92 | -------------------------------------------------------------------------------- /src/android/kotlin/RustWebView.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | @file:Suppress("unused", "SetJavaScriptEnabled") 6 | 7 | package {{package}} 8 | 9 | import android.annotation.SuppressLint 10 | import android.webkit.* 11 | import android.content.Context 12 | import androidx.webkit.WebViewCompat 13 | import androidx.webkit.WebViewFeature 14 | import kotlin.collections.Map 15 | 16 | @SuppressLint("RestrictedApi") 17 | class RustWebView(context: Context, val initScripts: Array, val id: String): WebView(context) { 18 | val isDocumentStartScriptEnabled: Boolean 19 | 20 | init { 21 | settings.javaScriptEnabled = true 22 | settings.domStorageEnabled = true 23 | settings.setGeolocationEnabled(true) 24 | settings.databaseEnabled = true 25 | settings.mediaPlaybackRequiresUserGesture = false 26 | settings.javaScriptCanOpenWindowsAutomatically = true 27 | 28 | if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { 29 | isDocumentStartScriptEnabled = true 30 | for (script in initScripts) { 31 | WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*")); 32 | } 33 | } else { 34 | isDocumentStartScriptEnabled = false 35 | } 36 | 37 | {{class-init}} 38 | } 39 | 40 | fun loadUrlMainThread(url: String) { 41 | post { 42 | loadUrl(url) 43 | } 44 | } 45 | 46 | fun loadUrlMainThread(url: String, additionalHttpHeaders: Map) { 47 | post { 48 | loadUrl(url, additionalHttpHeaders) 49 | } 50 | } 51 | 52 | override fun loadUrl(url: String) { 53 | if (!shouldOverride(url)) { 54 | super.loadUrl(url); 55 | } 56 | } 57 | 58 | override fun loadUrl(url: String, additionalHttpHeaders: Map) { 59 | if (!shouldOverride(url)) { 60 | super.loadUrl(url, additionalHttpHeaders); 61 | } 62 | } 63 | 64 | fun loadHTMLMainThread(html: String) { 65 | post { 66 | super.loadData(html, "text/html", null) 67 | } 68 | } 69 | 70 | fun evalScript(id: Int, script: String) { 71 | post { 72 | super.evaluateJavascript(script) { result -> 73 | onEval(id, result) 74 | } 75 | } 76 | } 77 | 78 | fun clearAllBrowsingData() { 79 | try { 80 | super.getContext().deleteDatabase("webviewCache.db") 81 | super.getContext().deleteDatabase("webview.db") 82 | super.clearCache(true) 83 | super.clearHistory() 84 | super.clearFormData() 85 | } catch (ex: Exception) { 86 | Logger.error("Unable to create temporary media capture file: " + ex.message) 87 | } 88 | } 89 | 90 | fun getCookies(url: String): String { 91 | val cookieManager = CookieManager.getInstance() 92 | return cookieManager.getCookie(url) 93 | } 94 | 95 | private external fun shouldOverride(url: String): Boolean 96 | private external fun onEval(id: Int, result: String) 97 | 98 | {{class-extension}} 99 | } 100 | -------------------------------------------------------------------------------- /examples/reparent.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use tao::{ 6 | event::{ElementState, Event, KeyEvent, WindowEvent}, 7 | event_loop::{ControlFlow, EventLoop}, 8 | keyboard::Key, 9 | window::WindowBuilder, 10 | }; 11 | use wry::WebViewBuilder; 12 | 13 | #[cfg(target_os = "macos")] 14 | use {objc2_app_kit::NSWindow, tao::platform::macos::WindowExtMacOS, wry::WebViewExtMacOS}; 15 | #[cfg(target_os = "windows")] 16 | use {tao::platform::windows::WindowExtWindows, wry::WebViewExtWindows}; 17 | 18 | #[cfg(not(any( 19 | target_os = "windows", 20 | target_os = "macos", 21 | target_os = "ios", 22 | target_os = "android" 23 | )))] 24 | #[cfg(not(any( 25 | target_os = "windows", 26 | target_os = "macos", 27 | target_os = "ios", 28 | target_os = "android" 29 | )))] 30 | use { 31 | tao::platform::unix::WindowExtUnix, 32 | wry::{WebViewBuilderExtUnix, WebViewExtUnix}, 33 | }; 34 | 35 | fn main() -> wry::Result<()> { 36 | let event_loop = EventLoop::new(); 37 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 38 | let window2 = WindowBuilder::new().build(&event_loop).unwrap(); 39 | 40 | let builder = WebViewBuilder::new().with_url("https://tauri.app"); 41 | 42 | #[cfg(any( 43 | target_os = "windows", 44 | target_os = "macos", 45 | target_os = "ios", 46 | target_os = "android" 47 | ))] 48 | let webview = builder.build(&window)?; 49 | #[cfg(not(any( 50 | target_os = "windows", 51 | target_os = "macos", 52 | target_os = "ios", 53 | target_os = "android" 54 | )))] 55 | let webview = { 56 | use tao::platform::unix::WindowExtUnix; 57 | let vbox = window.default_vbox().unwrap(); 58 | builder.build_gtk(vbox)? 59 | }; 60 | 61 | let mut webview_container = window.id(); 62 | 63 | event_loop.run(move |event, _event_loop, control_flow| { 64 | *control_flow = ControlFlow::Wait; 65 | 66 | match event { 67 | Event::WindowEvent { 68 | event: WindowEvent::CloseRequested, 69 | .. 70 | } => *control_flow = ControlFlow::Exit, 71 | 72 | Event::WindowEvent { 73 | event: 74 | WindowEvent::KeyboardInput { 75 | event: 76 | KeyEvent { 77 | logical_key: Key::Character("x"), 78 | state: ElementState::Pressed, 79 | .. 80 | }, 81 | .. 82 | }, 83 | .. 84 | } => { 85 | let new_parent = if webview_container == window.id() { 86 | &window2 87 | } else { 88 | &window 89 | }; 90 | webview_container = new_parent.id(); 91 | 92 | #[cfg(target_os = "macos")] 93 | webview 94 | .reparent(new_parent.ns_window() as *mut NSWindow) 95 | .unwrap(); 96 | #[cfg(not(any( 97 | target_os = "windows", 98 | target_os = "macos", 99 | target_os = "ios", 100 | target_os = "android" 101 | )))] 102 | webview 103 | .reparent(new_parent.default_vbox().unwrap()) 104 | .unwrap(); 105 | #[cfg(target_os = "windows")] 106 | webview.reparent(new_parent.hwnd()).unwrap(); 107 | } 108 | _ => {} 109 | } 110 | }); 111 | } 112 | -------------------------------------------------------------------------------- /examples/window_border.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use dpi::LogicalSize; 6 | use tao::{ 7 | event::{Event, StartCause, WindowEvent}, 8 | event_loop::{ControlFlow, EventLoopBuilder}, 9 | window::WindowBuilder, 10 | }; 11 | use wry::{http::Request, WebViewBuilder}; 12 | 13 | #[derive(Debug)] 14 | enum UserEvent { 15 | TogglShadows, 16 | } 17 | 18 | fn main() -> wry::Result<()> { 19 | let event_loop = EventLoopBuilder::::with_user_event().build(); 20 | let window = WindowBuilder::new() 21 | .with_inner_size(LogicalSize::new(500, 500)) 22 | .with_decorations(false) 23 | .build(&event_loop) 24 | .unwrap(); 25 | 26 | const HTML: &str = r#" 27 | 28 | 29 | 30 | 45 | 46 | 47 | 48 |

49 | Click the window to toggle shadows. 50 |

51 | 52 | 55 | 56 | 57 | 58 | "#; 59 | 60 | let proxy = event_loop.create_proxy(); 61 | let handler = move |req: Request| { 62 | if req.body().as_str() == "toggleShadows" { 63 | proxy.send_event(UserEvent::TogglShadows).unwrap(); 64 | } 65 | }; 66 | 67 | let builder = WebViewBuilder::new() 68 | .with_html(HTML) 69 | .with_ipc_handler(handler) 70 | .with_accept_first_mouse(true); 71 | 72 | #[cfg(any( 73 | target_os = "windows", 74 | target_os = "macos", 75 | target_os = "ios", 76 | target_os = "android" 77 | ))] 78 | let webview = builder.build(&window)?; 79 | #[cfg(not(any( 80 | target_os = "windows", 81 | target_os = "macos", 82 | target_os = "ios", 83 | target_os = "android" 84 | )))] 85 | let webview = { 86 | use tao::platform::unix::WindowExtUnix; 87 | use wry::WebViewBuilderExtUnix; 88 | let vbox = window.default_vbox().unwrap(); 89 | builder.build_gtk(vbox)? 90 | }; 91 | 92 | let mut webview = Some(webview); 93 | 94 | let mut shadow = true; 95 | 96 | event_loop.run(move |event, _, control_flow| { 97 | *control_flow = ControlFlow::Wait; 98 | 99 | match event { 100 | Event::NewEvents(StartCause::Init) => println!("Wry application started!"), 101 | Event::WindowEvent { 102 | event: WindowEvent::CloseRequested, 103 | .. 104 | } => { 105 | let _ = webview.take(); 106 | *control_flow = ControlFlow::Exit 107 | } 108 | 109 | Event::UserEvent(e) => match e { 110 | UserEvent::TogglShadows => { 111 | shadow = !shadow; 112 | #[cfg(windows)] 113 | { 114 | use tao::platform::windows::WindowExtWindows; 115 | window.set_undecorated_shadow(shadow); 116 | } 117 | } 118 | }, 119 | _ => (), 120 | } 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /src/web_context.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | #[cfg(gtk)] 6 | use crate::webkitgtk::WebContextImpl; 7 | 8 | use std::{ 9 | collections::HashSet, 10 | path::{Path, PathBuf}, 11 | }; 12 | 13 | /// A context that is shared between multiple [`WebView`]s. 14 | /// 15 | /// A browser would have a context for all the normal tabs and a different context for all the 16 | /// private/incognito tabs. 17 | /// 18 | /// # Warning 19 | /// If [`WebView`] is created by a WebContext. Dropping `WebContext` will cause [`WebView`] lose 20 | /// some actions like custom protocol on Mac. Please keep both instances when you still wish to 21 | /// interact with them. 22 | /// 23 | /// [`WebView`]: crate::WebView 24 | #[derive(Debug)] 25 | pub struct WebContext { 26 | data_directory: Option, 27 | #[allow(dead_code)] // It's not needed on Windows and macOS. 28 | pub(crate) os: WebContextImpl, 29 | #[allow(dead_code)] // It's not needed on Windows and macOS. 30 | pub(crate) custom_protocols: HashSet, 31 | } 32 | 33 | impl WebContext { 34 | /// Create a new [`WebContext`]. 35 | /// 36 | /// `data_directory`: 37 | /// * Whether the WebView window should have a custom user data path. This is useful in Windows 38 | /// when a bundled application can't have the webview data inside `Program Files`. 39 | pub fn new(data_directory: Option) -> Self { 40 | Self { 41 | os: WebContextImpl::new(data_directory.as_deref()), 42 | data_directory, 43 | custom_protocols: Default::default(), 44 | } 45 | } 46 | 47 | #[cfg(gtk)] 48 | pub(crate) fn new_ephemeral() -> Self { 49 | Self { 50 | os: WebContextImpl::new_ephemeral(), 51 | data_directory: None, 52 | custom_protocols: Default::default(), 53 | } 54 | } 55 | 56 | /// A reference to the data directory the context was created with. 57 | pub fn data_directory(&self) -> Option<&Path> { 58 | self.data_directory.as_deref() 59 | } 60 | 61 | #[cfg(any( 62 | target_os = "linux", 63 | target_os = "dragonfly", 64 | target_os = "freebsd", 65 | target_os = "netbsd", 66 | target_os = "openbsd", 67 | ))] 68 | pub(crate) fn register_custom_protocol(&mut self, name: String) -> Result<(), crate::Error> { 69 | if self.is_custom_protocol_registered(&name) { 70 | return Err(crate::Error::ContextDuplicateCustomProtocol(name)); 71 | } 72 | self.custom_protocols.insert(name); 73 | Ok(()) 74 | } 75 | 76 | /// Check if a custom protocol has been registered on this context. 77 | pub fn is_custom_protocol_registered(&self, name: &str) -> bool { 78 | self.custom_protocols.contains(name) 79 | } 80 | 81 | /// Set if this context allows automation. 82 | /// 83 | /// **Note:** This is currently only enforced on Linux, and has the stipulation that 84 | /// only 1 context allows automation at a time. 85 | pub fn set_allows_automation(&mut self, flag: bool) { 86 | self.os.set_allows_automation(flag); 87 | } 88 | } 89 | 90 | impl Default for WebContext { 91 | fn default() -> Self { 92 | Self::new(None) 93 | } 94 | } 95 | 96 | #[cfg(not(gtk))] 97 | #[derive(Debug)] 98 | pub(crate) struct WebContextImpl; 99 | 100 | #[cfg(not(gtk))] 101 | impl WebContextImpl { 102 | fn new(_: Option<&Path>) -> Self { 103 | Self 104 | } 105 | 106 | fn set_allows_automation(&mut self, _flag: bool) {} 107 | } 108 | -------------------------------------------------------------------------------- /examples/custom_protocol.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | fn main() -> wry::Result<()> { 6 | imp::main() 7 | } 8 | 9 | #[cfg(not(feature = "protocol"))] 10 | mod imp { 11 | pub fn main() -> wry::Result<()> { 12 | unimplemented!() 13 | } 14 | } 15 | 16 | #[cfg(feature = "protocol")] 17 | mod imp { 18 | use std::path::PathBuf; 19 | 20 | use tao::{ 21 | event::{Event, WindowEvent}, 22 | event_loop::{ControlFlow, EventLoop}, 23 | window::WindowBuilder, 24 | }; 25 | use wry::{ 26 | http::{header::CONTENT_TYPE, Request, Response}, 27 | WebViewBuilder, 28 | }; 29 | 30 | pub fn main() -> wry::Result<()> { 31 | let event_loop = EventLoop::new(); 32 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 33 | 34 | let builder = WebViewBuilder::new() 35 | .with_custom_protocol( 36 | "wry".into(), 37 | move |_webview_id, request| match get_wry_response(request) { 38 | Ok(r) => r.map(Into::into), 39 | Err(e) => http::Response::builder() 40 | .header(CONTENT_TYPE, "text/plain") 41 | .status(500) 42 | .body(e.to_string().as_bytes().to_vec()) 43 | .unwrap() 44 | .map(Into::into), 45 | }, 46 | ) 47 | // tell the webview to load the custom protocol 48 | .with_url("wry://localhost"); 49 | 50 | #[cfg(any( 51 | target_os = "windows", 52 | target_os = "macos", 53 | target_os = "ios", 54 | target_os = "android" 55 | ))] 56 | let _webview = builder.build(&window)?; 57 | #[cfg(not(any( 58 | target_os = "windows", 59 | target_os = "macos", 60 | target_os = "ios", 61 | target_os = "android" 62 | )))] 63 | let _webview = { 64 | use tao::platform::unix::WindowExtUnix; 65 | use wry::WebViewBuilderExtUnix; 66 | let vbox = window.default_vbox().unwrap(); 67 | builder.build_gtk(vbox)? 68 | }; 69 | 70 | event_loop.run(move |event, _, control_flow| { 71 | *control_flow = ControlFlow::Wait; 72 | 73 | if let Event::WindowEvent { 74 | event: WindowEvent::CloseRequested, 75 | .. 76 | } = event 77 | { 78 | *control_flow = ControlFlow::Exit 79 | } 80 | }); 81 | } 82 | 83 | fn get_wry_response( 84 | request: Request>, 85 | ) -> Result>, Box> { 86 | let path = request.uri().path(); 87 | // Read the file content from file path 88 | let root = PathBuf::from("examples/custom_protocol"); 89 | let path = if path == "/" { 90 | "index.html" 91 | } else { 92 | // removing leading slash 93 | &path[1..] 94 | }; 95 | let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; 96 | 97 | // Return asset contents and mime types based on file extentions 98 | // If you don't want to do this manually, there are some crates for you. 99 | // Such as `infer` and `mime_guess`. 100 | let mimetype = if path.ends_with(".html") || path == "/" { 101 | "text/html" 102 | } else if path.ends_with(".js") { 103 | "text/javascript" 104 | } else if path.ends_with(".png") { 105 | "image/png" 106 | } else if path.ends_with(".wasm") { 107 | "application/wasm" 108 | } else { 109 | unimplemented!(); 110 | }; 111 | 112 | Response::builder() 113 | .header(CONTENT_TYPE, mimetype) 114 | .body(content) 115 | .map_err(Into::into) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/webview2/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use once_cell::sync::Lazy; 6 | use windows::{ 7 | core::{HRESULT, HSTRING, PCSTR}, 8 | Win32::{ 9 | Foundation::{FARPROC, HWND, S_OK}, 10 | Graphics::Gdi::{ 11 | GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST, 12 | }, 13 | System::LibraryLoader::{GetProcAddress, LoadLibraryW}, 14 | UI::{ 15 | HiDpi::{MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE}, 16 | WindowsAndMessaging::IsProcessDPIAware, 17 | }, 18 | }, 19 | }; 20 | 21 | fn get_function_impl(library: &str, function: &str) -> FARPROC { 22 | let library = HSTRING::from(library); 23 | assert_eq!(function.chars().last(), Some('\0')); 24 | 25 | // Library names we will use are ASCII so we can use the A version to avoid string conversion. 26 | let module = unsafe { LoadLibraryW(&library) }.unwrap_or_default(); 27 | if module.is_invalid() { 28 | return None; 29 | } 30 | 31 | unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) } 32 | } 33 | 34 | macro_rules! get_function { 35 | ($lib:expr, $func:ident) => { 36 | crate::webview2::util::get_function_impl($lib, concat!(stringify!($func), '\0')) 37 | .map(|f| unsafe { std::mem::transmute::<_, $func>(f) }) 38 | }; 39 | } 40 | 41 | pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; 42 | pub type GetDpiForMonitor = unsafe extern "system" fn( 43 | hmonitor: HMONITOR, 44 | dpi_type: MONITOR_DPI_TYPE, 45 | dpi_x: *mut u32, 46 | dpi_y: *mut u32, 47 | ) -> HRESULT; 48 | 49 | static GET_DPI_FOR_WINDOW: Lazy> = 50 | Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); 51 | static GET_DPI_FOR_MONITOR: Lazy> = 52 | Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); 53 | 54 | pub const BASE_DPI: u32 = 96; 55 | pub fn dpi_to_scale_factor(dpi: u32) -> f64 { 56 | dpi as f64 / BASE_DPI as f64 57 | } 58 | 59 | #[allow(non_snake_case)] 60 | pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { 61 | if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { 62 | // We are on Windows 10 Anniversary Update (1607) or later. 63 | match GetDpiForWindow(hwnd) { 64 | 0 => BASE_DPI, // 0 is returned if hwnd is invalid 65 | #[allow(clippy::unnecessary_cast)] 66 | dpi => dpi as u32, 67 | } 68 | } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { 69 | // We are on Windows 8.1 or later. 70 | let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); 71 | if monitor.is_invalid() { 72 | return BASE_DPI; 73 | } 74 | 75 | let mut dpi_x = 0; 76 | let mut dpi_y = 0; 77 | #[allow(clippy::unnecessary_cast)] 78 | if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK { 79 | dpi_x as u32 80 | } else { 81 | BASE_DPI 82 | } 83 | } else { 84 | let hdc = GetDC(Some(hwnd)); 85 | if hdc.is_invalid() { 86 | return BASE_DPI; 87 | } 88 | 89 | // We are on Vista or later. 90 | if IsProcessDPIAware().as_bool() { 91 | // If the process is DPI aware, then scaling must be handled by the application using 92 | // this DPI value. 93 | GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32 94 | } else { 95 | // If the process is DPI unaware, then scaling is performed by the OS; we thus return 96 | // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the 97 | // application and the WM. 98 | BASE_DPI 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/async_custom_protocol.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | fn main() -> wry::Result<()> { 6 | imp::main() 7 | } 8 | 9 | #[cfg(not(feature = "protocol"))] 10 | mod imp { 11 | pub fn main() -> wry::Result<()> { 12 | unimplemented!() 13 | } 14 | } 15 | 16 | #[cfg(feature = "protocol")] 17 | mod imp { 18 | use std::path::PathBuf; 19 | 20 | use tao::{ 21 | event::{Event, WindowEvent}, 22 | event_loop::{ControlFlow, EventLoop}, 23 | window::WindowBuilder, 24 | }; 25 | use wry::{ 26 | http::{header::CONTENT_TYPE, Request, Response}, 27 | WebViewBuilder, 28 | }; 29 | 30 | pub fn main() -> wry::Result<()> { 31 | let event_loop = EventLoop::new(); 32 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 33 | 34 | let builder = WebViewBuilder::new() 35 | .with_asynchronous_custom_protocol("wry".into(), move |_webview_id, request, responder| { 36 | match get_wry_response(request) { 37 | Ok(http_response) => responder.respond(http_response), 38 | Err(e) => responder.respond( 39 | http::Response::builder() 40 | .header(CONTENT_TYPE, "text/plain") 41 | .status(500) 42 | .body(e.to_string().as_bytes().to_vec()) 43 | .unwrap(), 44 | ), 45 | } 46 | }) 47 | // tell the webview to load the custom protocol 48 | .with_url("wry://localhost"); 49 | 50 | #[cfg(any( 51 | target_os = "windows", 52 | target_os = "macos", 53 | target_os = "ios", 54 | target_os = "android" 55 | ))] 56 | let _webview = builder.build(&window)?; 57 | #[cfg(not(any( 58 | target_os = "windows", 59 | target_os = "macos", 60 | target_os = "ios", 61 | target_os = "android" 62 | )))] 63 | let _webview = { 64 | use tao::platform::unix::WindowExtUnix; 65 | use wry::WebViewBuilderExtUnix; 66 | let vbox = window.default_vbox().unwrap(); 67 | builder.build_gtk(vbox)? 68 | }; 69 | 70 | event_loop.run(move |event, _, control_flow| { 71 | *control_flow = ControlFlow::Wait; 72 | 73 | if let Event::WindowEvent { 74 | event: WindowEvent::CloseRequested, 75 | .. 76 | } = event 77 | { 78 | *control_flow = ControlFlow::Exit 79 | } 80 | }); 81 | } 82 | 83 | fn get_wry_response( 84 | request: Request>, 85 | ) -> Result>, Box> { 86 | let path = request.uri().path(); 87 | // Read the file content from file path 88 | let root = PathBuf::from("examples/custom_protocol"); 89 | let path = if path == "/" { 90 | "index.html" 91 | } else { 92 | // removing leading slash 93 | &path[1..] 94 | }; 95 | let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; 96 | 97 | // Return asset contents and mime types based on file extentions 98 | // If you don't want to do this manually, there are some crates for you. 99 | // Such as `infer` and `mime_guess`. 100 | let mimetype = if path.ends_with(".html") || path == "/" { 101 | "text/html" 102 | } else if path.ends_with(".js") { 103 | "text/javascript" 104 | } else if path.ends_with(".png") { 105 | "image/png" 106 | } else if path.ends_with(".wasm") { 107 | "application/wasm" 108 | } else { 109 | unimplemented!(); 110 | }; 111 | 112 | Response::builder() 113 | .header(CONTENT_TYPE, mimetype) 114 | .body(content) 115 | .map_err(Into::into) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/wkwebview/class/wry_web_view_delegate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::{ffi::CStr, panic::AssertUnwindSafe}; 6 | 7 | use http::Request; 8 | use objc2::{ 9 | define_class, msg_send, 10 | rc::Retained, 11 | runtime::{NSObject, ProtocolObject}, 12 | DeclaredClass, MainThreadOnly, 13 | }; 14 | use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSString}; 15 | use objc2_web_kit::{WKScriptMessage, WKScriptMessageHandler, WKUserContentController}; 16 | 17 | pub const IPC_MESSAGE_HANDLER_NAME: &str = "ipc"; 18 | 19 | pub struct WryWebViewDelegateIvars { 20 | pub controller: Retained, 21 | pub ipc_handler: Box)>, 22 | } 23 | 24 | define_class!( 25 | #[unsafe(super(NSObject))] 26 | #[name = "WryWebViewDelegate"] 27 | #[thread_kind = MainThreadOnly] 28 | #[ivars = WryWebViewDelegateIvars] 29 | pub struct WryWebViewDelegate; 30 | 31 | unsafe impl NSObjectProtocol for WryWebViewDelegate {} 32 | 33 | unsafe impl WKScriptMessageHandler for WryWebViewDelegate { 34 | // Function for ipc handler 35 | #[unsafe(method(userContentController:didReceiveScriptMessage:))] 36 | fn did_receive( 37 | this: &WryWebViewDelegate, 38 | _controller: &WKUserContentController, 39 | msg: &WKScriptMessage, 40 | ) { 41 | // Safety: objc runtime calls are unsafe 42 | unsafe { 43 | #[cfg(feature = "tracing")] 44 | let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered(); 45 | 46 | let ipc_handler = &this.ivars().ipc_handler; 47 | let body = msg.body(); 48 | if let Ok(body) = body.downcast::() { 49 | let js_utf8 = body.UTF8String(); 50 | 51 | let frame_info = msg.frameInfo(); 52 | let request = frame_info.request(); 53 | let url = request.URL().unwrap(); 54 | let absolute_url = url.absoluteString().unwrap(); 55 | let url_utf8 = absolute_url.UTF8String(); 56 | 57 | if let (Ok(url), Ok(js)) = ( 58 | CStr::from_ptr(url_utf8).to_str(), 59 | CStr::from_ptr(js_utf8).to_str(), 60 | ) { 61 | if let Ok(r) = Request::builder().uri(url).body(js.to_string()) { 62 | ipc_handler(r); 63 | } else { 64 | #[cfg(feature = "tracing")] 65 | tracing::warn!("WebView received invalid IPC request: {}", js); 66 | } 67 | return; 68 | } 69 | } 70 | 71 | #[cfg(feature = "tracing")] 72 | tracing::warn!("WebView received invalid IPC call."); 73 | } 74 | } 75 | } 76 | ); 77 | 78 | impl WryWebViewDelegate { 79 | pub fn new( 80 | controller: Retained, 81 | ipc_handler: Box)>, 82 | mtm: MainThreadMarker, 83 | ) -> Retained { 84 | let delegate = mtm 85 | .alloc::() 86 | .set_ivars(WryWebViewDelegateIvars { 87 | ipc_handler, 88 | controller, 89 | }); 90 | 91 | let delegate: Retained = unsafe { msg_send![super(delegate), init] }; 92 | 93 | let proto_delegate = ProtocolObject::from_ref(&*delegate); 94 | unsafe { 95 | // this will increase the retain count of the delegate 96 | let _res = objc2::exception::catch(AssertUnwindSafe(|| { 97 | delegate.ivars().controller.addScriptMessageHandler_name( 98 | proto_delegate, 99 | &NSString::from_str(IPC_MESSAGE_HANDLER_NAME), 100 | ); 101 | })); 102 | } 103 | 104 | delegate 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/wkwebview/navigation.rs: -------------------------------------------------------------------------------- 1 | use objc2::DeclaredClass; 2 | use objc2_foundation::{NSObjectProtocol, NSString}; 3 | use objc2_web_kit::{ 4 | WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationResponse, 5 | WKNavigationResponsePolicy, 6 | }; 7 | 8 | #[cfg(target_os = "ios")] 9 | use crate::wkwebview::ios::WKWebView::WKWebView; 10 | #[cfg(target_os = "macos")] 11 | use objc2_web_kit::WKWebView; 12 | 13 | use crate::PageLoadEvent; 14 | 15 | use super::class::wry_navigation_delegate::WryNavigationDelegate; 16 | 17 | pub(crate) fn did_commit_navigation( 18 | this: &WryNavigationDelegate, 19 | webview: &WKWebView, 20 | _navigation: &WKNavigation, 21 | ) { 22 | unsafe { 23 | // Call on_load_handler 24 | if let Some(on_page_load) = &this.ivars().on_page_load_handler { 25 | on_page_load(PageLoadEvent::Started); 26 | } 27 | 28 | // Inject scripts 29 | let mut pending_scripts = this.ivars().pending_scripts.lock().unwrap(); 30 | if let Some(scripts) = &*pending_scripts { 31 | for script in scripts { 32 | webview.evaluateJavaScript_completionHandler(&NSString::from_str(script), None); 33 | } 34 | *pending_scripts = None; 35 | } 36 | } 37 | } 38 | 39 | pub(crate) fn did_finish_navigation( 40 | this: &WryNavigationDelegate, 41 | _webview: &WKWebView, 42 | _navigation: &WKNavigation, 43 | ) { 44 | if let Some(on_page_load) = &this.ivars().on_page_load_handler { 45 | on_page_load(PageLoadEvent::Finished); 46 | } 47 | } 48 | 49 | // Navigation handler 50 | pub(crate) fn navigation_policy( 51 | this: &WryNavigationDelegate, 52 | _webview: &WKWebView, 53 | action: &WKNavigationAction, 54 | handler: &block2::Block, 55 | ) { 56 | unsafe { 57 | // shouldPerformDownload is only available on macOS 11.3+ 58 | let can_download = action.respondsToSelector(objc2::sel!(shouldPerformDownload)); 59 | let should_download: bool = if can_download { 60 | action.shouldPerformDownload() 61 | } else { 62 | false 63 | }; 64 | let request = action.request(); 65 | let url = request.URL().unwrap().absoluteString().unwrap(); 66 | 67 | if should_download { 68 | let has_download_handler = this.ivars().has_download_handler; 69 | if has_download_handler { 70 | (*handler).call((WKNavigationActionPolicy::Download,)); 71 | } else { 72 | (*handler).call((WKNavigationActionPolicy::Cancel,)); 73 | } 74 | } else { 75 | let function = &this.ivars().navigation_policy_function; 76 | match function(url.to_string()) { 77 | true => (*handler).call((WKNavigationActionPolicy::Allow,)), 78 | false => (*handler).call((WKNavigationActionPolicy::Cancel,)), 79 | }; 80 | } 81 | } 82 | } 83 | 84 | // Navigation handler 85 | pub(crate) fn navigation_policy_response( 86 | this: &WryNavigationDelegate, 87 | _webview: &WKWebView, 88 | response: &WKNavigationResponse, 89 | handler: &block2::Block, 90 | ) { 91 | unsafe { 92 | let can_show_mime_type = response.canShowMIMEType(); 93 | 94 | if !can_show_mime_type { 95 | let has_download_handler = this.ivars().has_download_handler; 96 | if has_download_handler { 97 | (*handler).call((WKNavigationResponsePolicy::Download,)); 98 | return; 99 | } 100 | } 101 | 102 | (*handler).call((WKNavigationResponsePolicy::Allow,)); 103 | } 104 | } 105 | 106 | pub(crate) fn web_content_process_did_terminate( 107 | this: &WryNavigationDelegate, 108 | _webview: &WKWebView, 109 | ) { 110 | if let Some(on_web_content_process_terminate) = 111 | &this.ivars().on_web_content_process_terminate_handler 112 | { 113 | on_web_content_process_terminate(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /examples/multiwindow.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::collections::HashMap; 6 | use tao::{ 7 | event::{Event, WindowEvent}, 8 | event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, 9 | window::{Window, WindowBuilder, WindowId}, 10 | }; 11 | use wry::{http::Request, WebView, WebViewBuilder}; 12 | 13 | enum UserEvent { 14 | CloseWindow(WindowId), 15 | NewTitle(WindowId, String), 16 | NewWindow, 17 | } 18 | 19 | fn main() -> wry::Result<()> { 20 | let event_loop = EventLoopBuilder::::with_user_event().build(); 21 | let mut webviews = HashMap::new(); 22 | let proxy = event_loop.create_proxy(); 23 | 24 | let (window, webview) = create_new_window( 25 | format!("Window {}", webviews.len() + 1), 26 | &event_loop, 27 | proxy.clone(), 28 | ); 29 | webviews.insert(window.id(), (window, webview)); 30 | 31 | event_loop.run(move |event, event_loop, control_flow| { 32 | *control_flow = ControlFlow::Wait; 33 | 34 | match event { 35 | Event::WindowEvent { 36 | event: WindowEvent::CloseRequested, 37 | window_id, 38 | .. 39 | } => { 40 | webviews.remove(&window_id); 41 | if webviews.is_empty() { 42 | *control_flow = ControlFlow::Exit 43 | } 44 | } 45 | Event::UserEvent(UserEvent::NewWindow) => { 46 | let (window, webview) = create_new_window( 47 | format!("Window {}", webviews.len() + 1), 48 | event_loop, 49 | proxy.clone(), 50 | ); 51 | webviews.insert(window.id(), (window, webview)); 52 | } 53 | Event::UserEvent(UserEvent::CloseWindow(id)) => { 54 | webviews.remove(&id); 55 | if webviews.is_empty() { 56 | *control_flow = ControlFlow::Exit 57 | } 58 | } 59 | 60 | Event::UserEvent(UserEvent::NewTitle(id, title)) => { 61 | webviews.get(&id).unwrap().0.set_title(&title); 62 | } 63 | _ => (), 64 | } 65 | }); 66 | } 67 | 68 | fn create_new_window( 69 | title: String, 70 | event_loop: &EventLoopWindowTarget, 71 | proxy: EventLoopProxy, 72 | ) -> (Window, WebView) { 73 | let window = WindowBuilder::new() 74 | .with_title(title) 75 | .build(event_loop) 76 | .unwrap(); 77 | let window_id = window.id(); 78 | let handler = move |req: Request| { 79 | let body = req.body(); 80 | match body.as_str() { 81 | "new-window" => { 82 | let _ = proxy.send_event(UserEvent::NewWindow); 83 | } 84 | "close" => { 85 | let _ = proxy.send_event(UserEvent::CloseWindow(window_id)); 86 | } 87 | _ if body.starts_with("change-title") => { 88 | let title = body.replace("change-title:", ""); 89 | let _ = proxy.send_event(UserEvent::NewTitle(window_id, title)); 90 | } 91 | _ => {} 92 | } 93 | }; 94 | 95 | let builder = WebViewBuilder::new() 96 | .with_html( 97 | r#" 98 | 99 | 100 | 101 | "#, 102 | ) 103 | .with_ipc_handler(handler); 104 | 105 | #[cfg(any( 106 | target_os = "windows", 107 | target_os = "macos", 108 | target_os = "ios", 109 | target_os = "android" 110 | ))] 111 | let webview = builder.build(&window).unwrap(); 112 | #[cfg(not(any( 113 | target_os = "windows", 114 | target_os = "macos", 115 | target_os = "ios", 116 | target_os = "android" 117 | )))] 118 | let webview = { 119 | use tao::platform::unix::WindowExtUnix; 120 | use wry::WebViewBuilderExtUnix; 121 | let vbox = window.default_vbox().unwrap(); 122 | builder.build_gtk(vbox).unwrap() 123 | }; 124 | (window, webview) 125 | } 126 | -------------------------------------------------------------------------------- /src/wkwebview/drag_drop.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::{ffi::CStr, path::PathBuf}; 6 | 7 | use objc2::{ 8 | runtime::{Bool, ProtocolObject}, 9 | DeclaredClass, 10 | }; 11 | use objc2_app_kit::{NSDragOperation, NSDraggingInfo, NSFilenamesPboardType}; 12 | use objc2_foundation::{NSArray, NSPoint, NSRect, NSString}; 13 | 14 | use crate::DragDropEvent; 15 | 16 | use super::WryWebView; 17 | 18 | pub(crate) unsafe fn collect_paths(drag_info: &ProtocolObject) -> Vec { 19 | let pb = drag_info.draggingPasteboard(); 20 | let mut drag_drop_paths = Vec::new(); 21 | let types = NSArray::arrayWithObject(NSFilenamesPboardType); 22 | 23 | if pb.availableTypeFromArray(&types).is_some() { 24 | let paths = pb.propertyListForType(NSFilenamesPboardType).unwrap(); 25 | let paths = paths.downcast::().unwrap(); 26 | for path in paths { 27 | let path = path.downcast::().unwrap(); 28 | let path = CStr::from_ptr(path.UTF8String()).to_string_lossy(); 29 | drag_drop_paths.push(PathBuf::from(path.into_owned())); 30 | } 31 | } 32 | drag_drop_paths 33 | } 34 | 35 | pub(crate) fn dragging_entered( 36 | this: &WryWebView, 37 | drag_info: &ProtocolObject, 38 | ) -> NSDragOperation { 39 | let paths = unsafe { collect_paths(drag_info) }; 40 | let dl: NSPoint = unsafe { drag_info.draggingLocation() }; 41 | let frame: NSRect = this.frame(); 42 | let position = (dl.x as i32, (frame.size.height - dl.y) as i32); 43 | 44 | let listener = &this.ivars().drag_drop_handler; 45 | if !listener(DragDropEvent::Enter { paths, position }) { 46 | // Reject the Wry file drop (invoke the OS default behaviour) 47 | unsafe { objc2::msg_send![super(this), draggingEntered: drag_info] } 48 | } else { 49 | NSDragOperation::Copy 50 | } 51 | } 52 | 53 | pub(crate) fn dragging_updated( 54 | this: &WryWebView, 55 | drag_info: &ProtocolObject, 56 | ) -> NSDragOperation { 57 | let dl: NSPoint = unsafe { drag_info.draggingLocation() }; 58 | let frame: NSRect = this.frame(); 59 | let position = (dl.x as i32, (frame.size.height - dl.y) as i32); 60 | 61 | let listener = &this.ivars().drag_drop_handler; 62 | if !listener(DragDropEvent::Over { position }) { 63 | unsafe { 64 | let os_operation = objc2::msg_send![super(this), draggingUpdated: drag_info]; 65 | if os_operation == NSDragOperation::None { 66 | // 0 will be returned for a drop on any arbitrary location on the webview. 67 | // We'll override that with NSDragOperationCopy. 68 | NSDragOperation::Copy 69 | } else { 70 | // A different NSDragOperation is returned when a file is hovered over something like 71 | // a , so we'll make sure to preserve that behaviour. 72 | os_operation 73 | } 74 | } 75 | } else { 76 | NSDragOperation::Copy 77 | } 78 | } 79 | 80 | pub(crate) fn perform_drag_operation( 81 | this: &WryWebView, 82 | drag_info: &ProtocolObject, 83 | ) -> Bool { 84 | let paths = unsafe { collect_paths(drag_info) }; 85 | let dl: NSPoint = unsafe { drag_info.draggingLocation() }; 86 | let frame: NSRect = this.frame(); 87 | let position = (dl.x as i32, (frame.size.height - dl.y) as i32); 88 | 89 | let listener = &this.ivars().drag_drop_handler; 90 | if !listener(DragDropEvent::Drop { paths, position }) { 91 | // Reject the Wry drop (invoke the OS default behaviour) 92 | unsafe { objc2::msg_send![super(this), performDragOperation: drag_info] } 93 | } else { 94 | Bool::YES 95 | } 96 | } 97 | 98 | pub(crate) fn dragging_exited(this: &WryWebView, drag_info: &ProtocolObject) { 99 | let listener = &this.ivars().drag_drop_handler; 100 | if !listener(DragDropEvent::Leave) { 101 | // Reject the Wry drop (invoke the OS default behaviour) 102 | unsafe { objc2::msg_send![super(this), draggingExited: drag_info] } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/wkwebview/class/wry_web_view_parent.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | #[cfg(target_os = "macos")] 6 | use objc2::DefinedClass; 7 | use objc2::{define_class, msg_send, rc::Retained, MainThreadOnly}; 8 | #[cfg(target_os = "macos")] 9 | use objc2_app_kit::{NSApplication, NSEvent, NSView, NSWindow, NSWindowButton}; 10 | use objc2_foundation::MainThreadMarker; 11 | #[cfg(target_os = "macos")] 12 | use objc2_foundation::NSRect; 13 | #[cfg(target_os = "ios")] 14 | use objc2_ui_kit::UIView as NSView; 15 | 16 | pub struct WryWebViewParentIvars { 17 | #[cfg(target_os = "macos")] 18 | traffic_light_inset: std::cell::Cell>, 19 | } 20 | 21 | define_class!( 22 | #[unsafe(super(NSView))] 23 | #[name = "WryWebViewParent"] 24 | #[ivars = WryWebViewParentIvars] 25 | pub struct WryWebViewParent; 26 | 27 | /// Overridden NSView methods. 28 | impl WryWebViewParent { 29 | #[cfg(target_os = "macos")] 30 | #[unsafe(method(keyDown:))] 31 | fn key_down(&self, event: &NSEvent) { 32 | let mtm = MainThreadMarker::new().unwrap(); 33 | let app = NSApplication::sharedApplication(mtm); 34 | unsafe { 35 | if let Some(menu) = app.mainMenu() { 36 | menu.performKeyEquivalent(event); 37 | } 38 | } 39 | } 40 | 41 | #[cfg(target_os = "macos")] 42 | #[unsafe(method(drawRect:))] 43 | fn draw(&self, _dirty_rect: NSRect) { 44 | if let Some((x, y)) = self.ivars().traffic_light_inset.get() { 45 | unsafe { inset_traffic_lights(&self.window().unwrap(), x, y) }; 46 | } 47 | } 48 | } 49 | ); 50 | 51 | impl WryWebViewParent { 52 | #[allow(dead_code)] 53 | pub fn new(mtm: MainThreadMarker) -> Retained { 54 | let delegate = WryWebViewParent::alloc(mtm).set_ivars(WryWebViewParentIvars { 55 | #[cfg(target_os = "macos")] 56 | traffic_light_inset: Default::default(), 57 | }); 58 | unsafe { msg_send![super(delegate), init] } 59 | } 60 | 61 | #[cfg(target_os = "macos")] 62 | pub fn set_traffic_light_inset(&self, ns_window: &NSWindow, position: dpi::Position) { 63 | let scale_factor = NSWindow::backingScaleFactor(ns_window); 64 | let position = position.to_logical(scale_factor); 65 | self 66 | .ivars() 67 | .traffic_light_inset 68 | .replace(Some((position.x, position.y))); 69 | 70 | unsafe { 71 | inset_traffic_lights(ns_window, position.x, position.y); 72 | } 73 | } 74 | } 75 | 76 | #[cfg(target_os = "macos")] 77 | pub unsafe fn inset_traffic_lights(window: &NSWindow, x: f64, y: f64) { 78 | let Some(close) = window.standardWindowButton(NSWindowButton::CloseButton) else { 79 | #[cfg(feature = "tracing")] 80 | tracing::warn!("skipping inset_traffic_lights, close button not found"); 81 | return; 82 | }; 83 | let Some(miniaturize) = window.standardWindowButton(NSWindowButton::MiniaturizeButton) else { 84 | #[cfg(feature = "tracing")] 85 | tracing::warn!("skipping inset_traffic_lights, miniaturize button not found"); 86 | return; 87 | }; 88 | let zoom = window.standardWindowButton(NSWindowButton::ZoomButton); 89 | 90 | let title_bar_container_view = close.superview().unwrap().superview().unwrap(); 91 | 92 | let close_rect = NSView::frame(&close); 93 | let title_bar_frame_height = close_rect.size.height + y; 94 | let mut title_bar_rect = NSView::frame(&title_bar_container_view); 95 | title_bar_rect.size.height = title_bar_frame_height; 96 | title_bar_rect.origin.y = window.frame().size.height - title_bar_frame_height; 97 | title_bar_container_view.setFrame(title_bar_rect); 98 | 99 | let space_between = NSView::frame(&miniaturize).origin.x - close_rect.origin.x; 100 | 101 | let mut window_buttons = vec![close, miniaturize]; 102 | if let Some(zoom) = zoom { 103 | window_buttons.push(zoom); 104 | } 105 | 106 | for (i, button) in window_buttons.into_iter().enumerate() { 107 | let mut rect = NSView::frame(&button); 108 | rect.origin.x = x + (i as f64 * space_between); 109 | button.setFrameOrigin(rect.origin); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/wkwebview/synthetic_mouse_events.rs: -------------------------------------------------------------------------------- 1 | use objc2_app_kit::{ 2 | NSAlternateKeyMask, NSCommandKeyMask, NSControlKeyMask, NSEvent, NSEventType, NSShiftKeyMask, 3 | NSView, 4 | }; 5 | use objc2_foundation::NSString; 6 | 7 | use super::WryWebView; 8 | 9 | pub(crate) fn other_mouse_down(this: &WryWebView, event: &NSEvent) { 10 | unsafe { 11 | if event.r#type() == NSEventType::OtherMouseDown { 12 | let button_number = event.buttonNumber(); 13 | match button_number { 14 | // back button 15 | 3 => { 16 | let js = create_js_mouse_event(this, event, true, true); 17 | this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None); 18 | return; 19 | } 20 | // forward button 21 | 4 => { 22 | let js = create_js_mouse_event(this, event, true, false); 23 | this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None); 24 | return; 25 | } 26 | _ => {} 27 | } 28 | } 29 | 30 | this.mouseDown(event); 31 | } 32 | } 33 | pub(crate) fn other_mouse_up(this: &WryWebView, event: &NSEvent) { 34 | unsafe { 35 | if event.r#type() == NSEventType::OtherMouseUp { 36 | let button_number = event.buttonNumber(); 37 | match button_number { 38 | // back button 39 | 3 => { 40 | let js = create_js_mouse_event(this, event, false, true); 41 | this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None); 42 | return; 43 | } 44 | // forward button 45 | 4 => { 46 | let js = create_js_mouse_event(this, event, false, false); 47 | this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None); 48 | return; 49 | } 50 | _ => {} 51 | } 52 | } 53 | 54 | this.mouseUp(event); 55 | } 56 | } 57 | 58 | unsafe fn create_js_mouse_event( 59 | view: &NSView, 60 | event: &NSEvent, 61 | down: bool, 62 | back_button: bool, 63 | ) -> String { 64 | let event_name = if down { "mousedown" } else { "mouseup" }; 65 | // js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button 66 | let button = if back_button { 3 } else { 4 }; 67 | let mods_flags = event.modifierFlags(); 68 | let window_point = event.locationInWindow(); 69 | let view_point = view.convertPoint_fromView(window_point, None); 70 | let x = view_point.x as u32; 71 | let y = view_point.y as u32; 72 | // js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons 73 | let buttons = NSEvent::pressedMouseButtons(); 74 | 75 | format!( 76 | r#"(() => {{ 77 | const el = document.elementFromPoint({x},{y}); 78 | const ev = new MouseEvent('{event_name}', {{ 79 | view: window, 80 | button: {button}, 81 | buttons: {buttons}, 82 | x: {x}, 83 | y: {y}, 84 | bubbles: true, 85 | detail: {detail}, 86 | cancelBubble: false, 87 | cancelable: true, 88 | clientX: {x}, 89 | clientY: {y}, 90 | composed: true, 91 | layerX: {x}, 92 | layerY: {y}, 93 | pageX: {x}, 94 | pageY: {y}, 95 | screenX: window.screenX + {x}, 96 | screenY: window.screenY + {y}, 97 | ctrlKey: {ctrl_key}, 98 | metaKey: {meta_key}, 99 | shiftKey: {shift_key}, 100 | altKey: {alt_key}, 101 | }}); 102 | el.dispatchEvent(ev) 103 | if (!ev.defaultPrevented && "{event_name}" === "mouseup") {{ 104 | if (ev.button === 3) {{ 105 | window.history.back(); 106 | }} 107 | if (ev.button === 4) {{ 108 | window.history.forward(); 109 | }} 110 | }} 111 | }})()"#, 112 | event_name = event_name, 113 | x = x, 114 | y = y, 115 | detail = event.clickCount(), 116 | ctrl_key = mods_flags.contains(NSControlKeyMask), 117 | alt_key = mods_flags.contains(NSAlternateKeyMask), 118 | shift_key = mods_flags.contains(NSShiftKeyMask), 119 | meta_key = mods_flags.contains(NSCommandKeyMask), 120 | button = button, 121 | buttons = buttons, 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /src/android/kotlin/RustWebViewClient.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | package {{package}} 6 | 7 | import android.net.Uri 8 | import android.webkit.* 9 | import android.content.Context 10 | import android.graphics.Bitmap 11 | import android.os.Handler 12 | import android.os.Looper 13 | import androidx.webkit.WebViewAssetLoader 14 | 15 | class RustWebViewClient(context: Context): WebViewClient() { 16 | private val interceptedState = mutableMapOf() 17 | var currentUrl: String = "about:blank" 18 | private var lastInterceptedUrl: Uri? = null 19 | private var pendingUrlRedirect: String? = null 20 | 21 | private val assetLoader = WebViewAssetLoader.Builder() 22 | .setDomain(assetLoaderDomain()) 23 | .addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context)) 24 | .build() 25 | 26 | override fun shouldInterceptRequest( 27 | view: WebView, 28 | request: WebResourceRequest 29 | ): WebResourceResponse? { 30 | pendingUrlRedirect?.let { 31 | Handler(Looper.getMainLooper()).post { 32 | view.loadUrl(it) 33 | } 34 | pendingUrlRedirect = null 35 | return null 36 | } 37 | 38 | lastInterceptedUrl = request.url 39 | return if (withAssetLoader()) { 40 | assetLoader.shouldInterceptRequest(request.url) 41 | } else { 42 | val rustWebview = view as RustWebView; 43 | val response = handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled) 44 | interceptedState[request.url.toString()] = response != null 45 | return response 46 | } 47 | } 48 | 49 | override fun shouldOverrideUrlLoading( 50 | view: WebView, 51 | request: WebResourceRequest 52 | ): Boolean { 53 | return shouldOverride(request.url.toString()) 54 | } 55 | 56 | override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { 57 | currentUrl = url 58 | if (interceptedState[url] == false) { 59 | val webView = view as RustWebView 60 | for (script in webView.initScripts) { 61 | view.evaluateJavascript(script, null) 62 | } 63 | } 64 | return onPageLoading(url) 65 | } 66 | 67 | override fun onPageFinished(view: WebView, url: String) { 68 | onPageLoaded(url) 69 | } 70 | 71 | override fun onReceivedError( 72 | view: WebView, 73 | request: WebResourceRequest, 74 | error: WebResourceError 75 | ) { 76 | // we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol 77 | // e.g. oauth flow, because shouldInterceptRequest is not called on redirects 78 | // so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in 79 | if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) { 80 | // prevent the default error page from showing 81 | view.stopLoading() 82 | // without this initial loadUrl the app is stuck 83 | view.loadUrl(request.url.toString()) 84 | // ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later 85 | pendingUrlRedirect = request.url.toString() 86 | } else { 87 | super.onReceivedError(view, request, error) 88 | } 89 | } 90 | 91 | companion object { 92 | init { 93 | System.loadLibrary("{{library}}") 94 | } 95 | } 96 | 97 | private external fun assetLoaderDomain(): String 98 | private external fun withAssetLoader(): Boolean 99 | private external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse? 100 | private external fun shouldOverride(url: String): Boolean 101 | private external fun onPageLoading(url: String) 102 | private external fun onPageLoaded(url: String) 103 | 104 | {{class-extension}} 105 | } 106 | -------------------------------------------------------------------------------- /src/webkitgtk/drag_drop.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::{ 6 | cell::{Cell, UnsafeCell}, 7 | path::PathBuf, 8 | rc::Rc, 9 | }; 10 | 11 | use gtk::{glib::GString, prelude::*}; 12 | use webkit2gtk::WebView; 13 | 14 | use crate::DragDropEvent; 15 | 16 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)] 17 | enum DragControllerState { 18 | Entered, 19 | Leaving, 20 | Left, 21 | } 22 | 23 | struct DragDropController { 24 | paths: UnsafeCell>>, 25 | state: Cell, 26 | position: Cell<(i32, i32)>, 27 | handler: Box bool>, 28 | } 29 | 30 | impl DragDropController { 31 | fn new(handler: Box bool>) -> Self { 32 | Self { 33 | handler, 34 | paths: UnsafeCell::new(None), 35 | state: Cell::new(DragControllerState::Left), 36 | position: Cell::new((0, 0)), 37 | } 38 | } 39 | 40 | fn store_paths(&self, paths: Vec) { 41 | unsafe { *self.paths.get() = Some(paths) }; 42 | } 43 | 44 | fn take_paths(&self) -> Option> { 45 | unsafe { &mut *self.paths.get() }.take() 46 | } 47 | 48 | fn store_position(&self, position: (i32, i32)) { 49 | self.position.replace(position); 50 | } 51 | 52 | fn enter(&self) { 53 | self.state.set(DragControllerState::Entered); 54 | } 55 | 56 | fn leaving(&self) { 57 | self.state.set(DragControllerState::Leaving); 58 | } 59 | 60 | fn leave(&self) { 61 | self.state.set(DragControllerState::Left); 62 | } 63 | 64 | fn state(&self) -> DragControllerState { 65 | self.state.get() 66 | } 67 | 68 | fn call(&self, event: DragDropEvent) -> bool { 69 | (self.handler)(event) 70 | } 71 | } 72 | 73 | pub(crate) fn connect_drag_event(webview: &WebView, handler: Box bool>) { 74 | let controller = Rc::new(DragDropController::new(handler)); 75 | 76 | { 77 | let controller = controller.clone(); 78 | webview.connect_drag_data_received(move |_, _, _, _, data, info, _| { 79 | if info == 2 { 80 | let uris = data.uris(); 81 | let paths = uris.iter().map(path_buf_from_uri).collect::>(); 82 | controller.enter(); 83 | controller.call(DragDropEvent::Enter { 84 | paths: paths.clone(), 85 | position: controller.position.get(), 86 | }); 87 | controller.store_paths(paths); 88 | } 89 | }); 90 | } 91 | 92 | { 93 | let controller = controller.clone(); 94 | webview.connect_drag_motion(move |_, _, x, y, _| { 95 | if controller.state() == DragControllerState::Entered { 96 | controller.call(DragDropEvent::Over { position: (x, y) }); 97 | } else { 98 | controller.store_position((x, y)); 99 | } 100 | false 101 | }); 102 | } 103 | 104 | { 105 | let controller = controller.clone(); 106 | webview.connect_drag_drop(move |_, ctx, x, y, time| { 107 | if controller.state() == DragControllerState::Leaving { 108 | if let Some(paths) = controller.take_paths() { 109 | ctx.drop_finish(true, time); 110 | controller.leave(); 111 | return controller.call(DragDropEvent::Drop { 112 | paths, 113 | position: (x, y), 114 | }); 115 | } 116 | } 117 | 118 | false 119 | }); 120 | } 121 | 122 | webview.connect_drag_leave(move |_w, _, _| { 123 | if controller.state() != DragControllerState::Left { 124 | controller.leaving(); 125 | let controller = controller.clone(); 126 | gtk::glib::idle_add_local_once(move || { 127 | if controller.state() == DragControllerState::Leaving { 128 | controller.leave(); 129 | controller.call(DragDropEvent::Leave); 130 | } 131 | }); 132 | } 133 | }); 134 | } 135 | 136 | fn path_buf_from_uri(gstr: &GString) -> PathBuf { 137 | let path = gstr.as_str(); 138 | let path = path.strip_prefix("file://").unwrap_or(path); 139 | let path = percent_encoding::percent_decode(path.as_bytes()) 140 | .decode_utf8_lossy() 141 | .to_string(); 142 | PathBuf::from(path) 143 | } 144 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | platform: 15 | - { target: x86_64-pc-windows-msvc, os: windows-latest } 16 | - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest } 17 | - { target: aarch64-apple-darwin, os: macos-latest } 18 | - { target: aarch64-apple-ios, os: macos-latest } 19 | - { target: x86_64-apple-darwin, os: macos-latest } 20 | - { target: aarch64-linux-android, os: ubuntu-latest } 21 | features: 22 | - { 23 | args: --no-default-features --features os-webview, 24 | key: no-default, 25 | } 26 | - { args: --all-features, key: all } 27 | 28 | runs-on: ${{ matrix.platform.os }} 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: install stable 33 | uses: dtolnay/rust-toolchain@1.77 34 | with: 35 | targets: ${{ matrix.platform.target }} 36 | 37 | - name: install webkit2gtk (ubuntu only) 38 | if: contains(matrix.platform.target, 'gnu') 39 | run: | 40 | sudo apt-get update 41 | sudo apt-get install -y libwebkit2gtk-4.1-dev 42 | 43 | - name: install webview2 (windows only) 44 | if: contains(matrix.platform.target, 'windows') 45 | shell: pwsh 46 | run: | 47 | Invoke-WebRequest https://go.microsoft.com/fwlink/p/?LinkId=2124703 -OutFile installwebview.exe -UseBasicParsing 48 | cmd /C start /wait installwebview.exe /silent /install 49 | 50 | - uses: Swatinem/rust-cache@v2 51 | with: 52 | save-if: ${{ matrix.features.key == 'all' }} 53 | 54 | - name: build wry 55 | run: cargo build --target ${{ matrix.platform.target }} ${{ matrix.features.args }} 56 | 57 | - name: build tests and examples 58 | shell: bash 59 | if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios')) 60 | run: cargo test --no-run --verbose --target ${{ matrix.platform.target }} ${{ matrix.features.args }} 61 | 62 | - name: run tests 63 | if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios')) 64 | run: cargo test --verbose --target ${{ matrix.platform.target }} --features linux-body ${{ matrix.features.args }} 65 | 66 | - name: install nightly 67 | uses: dtolnay/rust-toolchain@nightly 68 | with: 69 | targets: ${{ matrix.platform.target }} 70 | components: miri 71 | 72 | - name: Run tests with miri 73 | if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios')) 74 | run: cargo +nightly miri test --verbose --target ${{ matrix.platform.target }} --features linux-body ${{ matrix.features.args }} 75 | 76 | doc: 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v4 80 | - name: install nightly 81 | uses: dtolnay/rust-toolchain@nightly 82 | - name: install webkit2gtk 83 | run: | 84 | sudo apt-get update -y -q 85 | sudo apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev 86 | - name: Run cargo doc with each targets 87 | env: 88 | DOCS_RS: true 89 | run: | 90 | set -ex 91 | 92 | md="$(cargo metadata --format-version=1 | jq '.packages[] | select(.name=="wry") | .metadata.docs.rs')" 93 | 94 | export RUSTDOCFLAGS="$(echo "$md" | jq -r '.["rustdoc-args"] | join(" ")')" 95 | export RUSTFLAGS="$(echo "$md" | jq -r '.["rustc-args"] | join(" ")')" 96 | 97 | features="$(echo "$md" | jq -r '.features | join(",")')" 98 | no_default_features= 99 | if [ "$(echo "$md" | jq '.["no-default-features"]')" = true ]; then 100 | no_default_features="--no-default-features" 101 | fi 102 | 103 | for target in $(echo "$md" | jq -r '.targets | join(" ")') 104 | do 105 | rustup target add "$target" 106 | cargo doc -p wry "$no_default_features" "--features=$features" "--target=$target" 107 | done 108 | -------------------------------------------------------------------------------- /src/wkwebview/download.rs: -------------------------------------------------------------------------------- 1 | use std::{env::current_dir, ptr::null_mut}; 2 | 3 | use objc2::{rc::Retained, runtime::ProtocolObject, DeclaredClass}; 4 | use objc2_foundation::{NSData, NSError, NSString, NSURLResponse, NSURL}; 5 | use objc2_web_kit::{WKDownload, WKNavigationAction, WKNavigationResponse}; 6 | 7 | #[cfg(target_os = "ios")] 8 | use crate::wkwebview::ios::WKWebView::WKWebView; 9 | #[cfg(target_os = "macos")] 10 | use objc2_web_kit::WKWebView; 11 | 12 | use super::class::{ 13 | wry_download_delegate::WryDownloadDelegate, wry_navigation_delegate::WryNavigationDelegate, 14 | }; 15 | 16 | // Download action handler 17 | pub(crate) fn navigation_download_action( 18 | this: &WryNavigationDelegate, 19 | _webview: &WKWebView, 20 | _action: &WKNavigationAction, 21 | download: &WKDownload, 22 | ) { 23 | unsafe { 24 | if let Some(delegate) = &this.ivars().download_delegate { 25 | let proto_delegate = ProtocolObject::from_ref(&**delegate); 26 | download.setDelegate(Some(proto_delegate)); 27 | } 28 | } 29 | } 30 | 31 | // Download response handler 32 | pub(crate) fn navigation_download_response( 33 | this: &WryNavigationDelegate, 34 | _webview: &WKWebView, 35 | _response: &WKNavigationResponse, 36 | download: &WKDownload, 37 | ) { 38 | unsafe { 39 | if let Some(delegate) = &this.ivars().download_delegate { 40 | let proto_delegate = ProtocolObject::from_ref(&**delegate); 41 | download.setDelegate(Some(proto_delegate)); 42 | } 43 | } 44 | } 45 | 46 | pub(crate) fn download_policy( 47 | this: &WryDownloadDelegate, 48 | download: &WKDownload, 49 | _response: &NSURLResponse, 50 | suggested_filename: &NSString, 51 | completion_handler: &block2::Block, 52 | ) { 53 | unsafe { 54 | let request = download.originalRequest().unwrap(); 55 | let url = request.URL().unwrap().absoluteString().unwrap(); 56 | let suggested_filename = suggested_filename.to_string(); 57 | let mut download_destination = 58 | dirs::download_dir().unwrap_or_else(|| current_dir().unwrap_or_default()); 59 | 60 | download_destination.push(&suggested_filename); 61 | 62 | let (suggested_filename, ext) = suggested_filename 63 | .split_once('.') 64 | .map(|(base, ext)| (base, format!(".{ext}"))) 65 | .unwrap_or((&suggested_filename, "".to_string())); 66 | 67 | // WebView2 does not overwrite files but appends numbers 68 | let mut counter = 1; 69 | while download_destination.exists() { 70 | download_destination.set_file_name(format!("{suggested_filename} ({counter}){ext}")); 71 | counter += 1; 72 | } 73 | 74 | let started_fn = &this.ivars().started; 75 | if let Some(started_fn) = started_fn { 76 | let mut started_fn = started_fn.borrow_mut(); 77 | match started_fn(url.to_string().to_string(), &mut download_destination) { 78 | true => { 79 | let path = NSString::from_str(&download_destination.display().to_string()); 80 | let ns_url = NSURL::fileURLWithPath_isDirectory(&path, false); 81 | (*completion_handler).call((Retained::as_ptr(&ns_url),)) 82 | } 83 | false => (*completion_handler).call((null_mut(),)), 84 | }; 85 | } else { 86 | #[cfg(feature = "tracing")] 87 | tracing::warn!("WebView instance is dropped! This navigation handler shouldn't be called."); 88 | (*completion_handler).call((null_mut(),)); 89 | } 90 | } 91 | } 92 | 93 | pub(crate) fn download_did_finish(this: &WryDownloadDelegate, download: &WKDownload) { 94 | unsafe { 95 | let original_request = download.originalRequest().unwrap(); 96 | let url = original_request.URL().unwrap().absoluteString().unwrap(); 97 | if let Some(completed_fn) = this.ivars().completed.clone() { 98 | completed_fn(url.to_string(), None, true); 99 | } 100 | } 101 | } 102 | 103 | pub(crate) fn download_did_fail( 104 | this: &WryDownloadDelegate, 105 | download: &WKDownload, 106 | error: &NSError, 107 | _resume_data: &NSData, 108 | ) { 109 | unsafe { 110 | #[cfg(debug_assertions)] 111 | { 112 | let description = error.localizedDescription().to_string(); 113 | eprintln!("Download failed with error: {description}"); 114 | } 115 | 116 | let original_request = download.originalRequest().unwrap(); 117 | let url = original_request.URL().unwrap().absoluteString().unwrap(); 118 | if let Some(completed_fn) = this.ivars().completed.clone() { 119 | completed_fn(url.to_string(), None, false); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /examples/gtk_multiwebview.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use tao::{ 6 | event::{Event, WindowEvent}, 7 | event_loop::{ControlFlow, EventLoop}, 8 | window::WindowBuilder, 9 | }; 10 | use wry::{ 11 | dpi::{LogicalPosition, LogicalSize}, 12 | Rect, WebViewBuilder, 13 | }; 14 | 15 | fn main() -> wry::Result<()> { 16 | let event_loop = EventLoop::new(); 17 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 18 | 19 | #[cfg(not(any( 20 | target_os = "windows", 21 | target_os = "macos", 22 | target_os = "ios", 23 | target_os = "android" 24 | )))] 25 | let fixed = { 26 | use gtk::prelude::*; 27 | use tao::platform::unix::WindowExtUnix; 28 | let fixed = gtk::Fixed::new(); 29 | let vbox = window.default_vbox().unwrap(); 30 | vbox.pack_start(&fixed, true, true, 0); 31 | fixed.show_all(); 32 | fixed 33 | }; 34 | 35 | let build_webview = |builder: WebViewBuilder<'_>| -> wry::Result { 36 | #[cfg(any( 37 | target_os = "windows", 38 | target_os = "macos", 39 | target_os = "ios", 40 | target_os = "android" 41 | ))] 42 | let webview = builder.build(&window)?; 43 | 44 | #[cfg(not(any( 45 | target_os = "windows", 46 | target_os = "macos", 47 | target_os = "ios", 48 | target_os = "android" 49 | )))] 50 | let webview = { 51 | use wry::WebViewBuilderExtUnix; 52 | builder.build_gtk(&fixed)? 53 | }; 54 | 55 | Ok(webview) 56 | }; 57 | 58 | let size = window.inner_size().to_logical::(window.scale_factor()); 59 | 60 | let builder = WebViewBuilder::new() 61 | .with_bounds(Rect { 62 | position: LogicalPosition::new(0, 0).into(), 63 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 64 | }) 65 | .with_url("https://tauri.app"); 66 | let webview = build_webview(builder)?; 67 | 68 | let builder2 = WebViewBuilder::new() 69 | .with_bounds(Rect { 70 | position: LogicalPosition::new(size.width / 2, 0).into(), 71 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 72 | }) 73 | .with_url("https://github.com/tauri-apps/wry"); 74 | let webview2 = build_webview(builder2)?; 75 | 76 | let builder3 = WebViewBuilder::new() 77 | .with_bounds(Rect { 78 | position: LogicalPosition::new(0, size.height / 2).into(), 79 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 80 | }) 81 | .with_url("https://twitter.com/TauriApps"); 82 | let webview3 = build_webview(builder3)?; 83 | 84 | let builder4 = WebViewBuilder::new() 85 | .with_bounds(Rect { 86 | position: LogicalPosition::new(size.width / 2, size.height / 2).into(), 87 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 88 | }) 89 | .with_url("https://google.com"); 90 | let webview4 = build_webview(builder4)?; 91 | 92 | event_loop.run(move |event, _, control_flow| { 93 | *control_flow = ControlFlow::Wait; 94 | 95 | match event { 96 | Event::WindowEvent { 97 | event: WindowEvent::Resized(size), 98 | .. 99 | } => { 100 | let size = size.to_logical::(window.scale_factor()); 101 | webview 102 | .set_bounds(Rect { 103 | position: LogicalPosition::new(0, 0).into(), 104 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 105 | }) 106 | .unwrap(); 107 | webview2 108 | .set_bounds(Rect { 109 | position: LogicalPosition::new(size.width / 2, 0).into(), 110 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 111 | }) 112 | .unwrap(); 113 | webview3 114 | .set_bounds(Rect { 115 | position: LogicalPosition::new(0, size.height / 2).into(), 116 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 117 | }) 118 | .unwrap(); 119 | webview4 120 | .set_bounds(Rect { 121 | position: LogicalPosition::new(size.width / 2, size.height / 2).into(), 122 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 123 | }) 124 | .unwrap(); 125 | } 126 | Event::WindowEvent { 127 | event: WindowEvent::CloseRequested, 128 | .. 129 | } => *control_flow = ControlFlow::Exit, 130 | _ => {} 131 | } 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /src/android/kotlin/PermissionHelper.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | package {{package}} 6 | 7 | // taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java 8 | 9 | import android.content.Context 10 | import android.content.pm.PackageManager 11 | import android.os.Build 12 | import androidx.core.app.ActivityCompat 13 | import java.util.ArrayList 14 | 15 | object PermissionHelper { 16 | /** 17 | * Checks if a list of given permissions are all granted by the user 18 | * 19 | * @param permissions Permissions to check. 20 | * @return True if all permissions are granted, false if at least one is not. 21 | */ 22 | fun hasPermissions(context: Context?, permissions: Array): Boolean { 23 | for (perm in permissions) { 24 | if (ActivityCompat.checkSelfPermission( 25 | context!!, 26 | perm 27 | ) != PackageManager.PERMISSION_GRANTED 28 | ) { 29 | return false 30 | } 31 | } 32 | return true 33 | } 34 | 35 | /** 36 | * Check whether the given permission has been defined in the AndroidManifest.xml 37 | * 38 | * @param permission A permission to check. 39 | * @return True if the permission has been defined in the Manifest, false if not. 40 | */ 41 | fun hasDefinedPermission(context: Context, permission: String): Boolean { 42 | var hasPermission = false 43 | val requestedPermissions = getManifestPermissions(context) 44 | if (!requestedPermissions.isNullOrEmpty()) { 45 | val requestedPermissionsList = listOf(*requestedPermissions) 46 | val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) 47 | if (requestedPermissionsArrayList.contains(permission)) { 48 | hasPermission = true 49 | } 50 | } 51 | return hasPermission 52 | } 53 | 54 | /** 55 | * Check whether all of the given permissions have been defined in the AndroidManifest.xml 56 | * @param context the app context 57 | * @param permissions a list of permissions 58 | * @return true only if all permissions are defined in the AndroidManifest.xml 59 | */ 60 | fun hasDefinedPermissions(context: Context, permissions: Array): Boolean { 61 | for (permission in permissions) { 62 | if (!hasDefinedPermission(context, permission)) { 63 | return false 64 | } 65 | } 66 | return true 67 | } 68 | 69 | /** 70 | * Get the permissions defined in AndroidManifest.xml 71 | * 72 | * @return The permissions defined in AndroidManifest.xml 73 | */ 74 | private fun getManifestPermissions(context: Context): Array? { 75 | var requestedPermissions: Array? = null 76 | try { 77 | val pm = context.packageManager 78 | val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 79 | pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())) 80 | } else { 81 | @Suppress("DEPRECATION") 82 | pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) 83 | } 84 | if (packageInfo != null) { 85 | requestedPermissions = packageInfo.requestedPermissions 86 | } 87 | } catch (_: Exception) { 88 | } 89 | return requestedPermissions 90 | } 91 | 92 | /** 93 | * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml 94 | * 95 | * @param neededPermissions The permissions needed. 96 | * @return The permissions not present in AndroidManifest.xml 97 | */ 98 | fun getUndefinedPermissions(context: Context, neededPermissions: Array): Array { 99 | val undefinedPermissions = ArrayList() 100 | val requestedPermissions = getManifestPermissions(context) 101 | if (!requestedPermissions.isNullOrEmpty()) { 102 | val requestedPermissionsList = listOf(*requestedPermissions) 103 | val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) 104 | for (permission in neededPermissions) { 105 | if (!requestedPermissionsArrayList.contains(permission)) { 106 | undefinedPermissions.add(permission) 107 | } 108 | } 109 | var undefinedPermissionArray = arrayOfNulls(undefinedPermissions.size) 110 | undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray) 111 | return undefinedPermissionArray 112 | } 113 | return neededPermissions 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/android/kotlin/WryActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | package {{package}} 6 | 7 | import {{package}}.RustWebView 8 | import android.annotation.SuppressLint 9 | import android.os.Build 10 | import android.os.Bundle 11 | import android.webkit.WebView 12 | import android.view.KeyEvent 13 | import androidx.activity.OnBackPressedCallback 14 | import androidx.appcompat.app.AppCompatActivity 15 | 16 | abstract class WryActivity : AppCompatActivity() { 17 | private lateinit var mWebView: RustWebView 18 | open val handleBackNavigation: Boolean = true 19 | 20 | open fun onWebViewCreate(webView: WebView) { } 21 | 22 | fun setWebView(webView: RustWebView) { 23 | mWebView = webView 24 | 25 | if (handleBackNavigation) { 26 | val callback = object : OnBackPressedCallback(true) { 27 | override fun handleOnBackPressed() { 28 | if (this@WryActivity.mWebView.canGoBack()) { 29 | this@WryActivity.mWebView.goBack() 30 | } else { 31 | this.isEnabled = false 32 | this@WryActivity.onBackPressed() 33 | this.isEnabled = true 34 | } 35 | } 36 | } 37 | onBackPressedDispatcher.addCallback(this, callback) 38 | } 39 | 40 | onWebViewCreate(webView) 41 | } 42 | 43 | val version: String 44 | @SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt") 45 | get() { 46 | // Check getCurrentWebViewPackage() directly if above Android 8 47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 48 | return WebView.getCurrentWebViewPackage()?.versionName ?: "" 49 | } 50 | 51 | // Otherwise manually check WebView versions 52 | var webViewPackage = "com.google.android.webview" 53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 54 | webViewPackage = "com.android.chrome" 55 | } 56 | try { 57 | @Suppress("DEPRECATION") 58 | val info = packageManager.getPackageInfo(webViewPackage, 0) 59 | return info.versionName.toString() 60 | } catch (ex: Exception) { 61 | Logger.warn("Unable to get package info for '$webViewPackage'$ex") 62 | } 63 | 64 | try { 65 | @Suppress("DEPRECATION") 66 | val info = packageManager.getPackageInfo("com.android.webview", 0) 67 | return info.versionName.toString() 68 | } catch (ex: Exception) { 69 | Logger.warn("Unable to get package info for 'com.android.webview'$ex") 70 | } 71 | 72 | // Could not detect any webview, return empty string 73 | return "" 74 | } 75 | 76 | override fun onCreate(savedInstanceState: Bundle?) { 77 | super.onCreate(savedInstanceState) 78 | create(this) 79 | } 80 | 81 | override fun onStart() { 82 | super.onStart() 83 | start() 84 | } 85 | 86 | override fun onResume() { 87 | super.onResume() 88 | resume() 89 | } 90 | 91 | override fun onPause() { 92 | super.onPause() 93 | pause() 94 | } 95 | 96 | override fun onStop() { 97 | super.onStop() 98 | stop() 99 | } 100 | 101 | override fun onWindowFocusChanged(hasFocus: Boolean) { 102 | super.onWindowFocusChanged(hasFocus) 103 | focus(hasFocus) 104 | } 105 | 106 | override fun onSaveInstanceState(outState: Bundle) { 107 | super.onSaveInstanceState(outState) 108 | save() 109 | } 110 | 111 | override fun onDestroy() { 112 | super.onDestroy() 113 | destroy() 114 | onActivityDestroy() 115 | } 116 | 117 | override fun onLowMemory() { 118 | super.onLowMemory() 119 | memory() 120 | } 121 | 122 | fun getAppClass(name: String): Class<*> { 123 | return Class.forName(name) 124 | } 125 | 126 | companion object { 127 | init { 128 | System.loadLibrary("{{library}}") 129 | } 130 | } 131 | 132 | private external fun create(activity: WryActivity) 133 | private external fun start() 134 | private external fun resume() 135 | private external fun pause() 136 | private external fun stop() 137 | private external fun save() 138 | private external fun destroy() 139 | private external fun onActivityDestroy() 140 | private external fun memory() 141 | private external fun focus(focus: Boolean) 142 | 143 | {{class-extension}} 144 | } 145 | -------------------------------------------------------------------------------- /src/wkwebview/class/wry_web_view.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::{collections::HashMap, sync::Mutex}; 6 | 7 | #[cfg(target_os = "macos")] 8 | use objc2::runtime::ProtocolObject; 9 | use objc2::{define_class, rc::Retained, runtime::Bool, DeclaredClass}; 10 | #[cfg(target_os = "macos")] 11 | use objc2_app_kit::{NSDraggingDestination, NSEvent}; 12 | use objc2_foundation::{NSObjectProtocol, NSUUID}; 13 | 14 | #[cfg(target_os = "ios")] 15 | use crate::wkwebview::ios::WKWebView::WKWebView; 16 | #[cfg(target_os = "macos")] 17 | use crate::{ 18 | wkwebview::{drag_drop, synthetic_mouse_events}, 19 | DragDropEvent, 20 | }; 21 | #[cfg(target_os = "ios")] 22 | use objc2_ui_kit::UIEvent as NSEvent; 23 | #[cfg(target_os = "macos")] 24 | use objc2_web_kit::WKWebView; 25 | 26 | pub struct WryWebViewIvars { 27 | pub(crate) is_child: bool, 28 | #[cfg(target_os = "macos")] 29 | pub(crate) drag_drop_handler: Box bool>, 30 | #[cfg(target_os = "macos")] 31 | pub(crate) accept_first_mouse: objc2::runtime::Bool, 32 | #[cfg(target_os = "ios")] 33 | pub(crate) input_accessory_view_builder: Option>, 34 | pub(crate) custom_protocol_task_ids: Mutex>>, 35 | } 36 | 37 | define_class!( 38 | #[unsafe(super(WKWebView))] 39 | #[name = "WryWebView"] 40 | #[ivars = WryWebViewIvars] 41 | pub struct WryWebView; 42 | 43 | /// Overridden NSView methods. 44 | impl WryWebView { 45 | #[unsafe(method(performKeyEquivalent:))] 46 | fn perform_key_equivalent(&self, event: &NSEvent) -> Bool { 47 | // This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426 48 | // FIXME: When the webview is a child webview, performKeyEquivalent always return YES 49 | // and stop propagating the event to the window, hence the menu shortcut won't be 50 | // triggered. However, overriding this method also means the cmd+key event won't be 51 | // handled in webview, which means the key cannot be listened by JavaScript. 52 | if self.ivars().is_child { 53 | Bool::NO 54 | } else { 55 | unsafe { objc2::msg_send![super(self), performKeyEquivalent: event] } 56 | } 57 | } 58 | 59 | #[cfg(target_os = "macos")] 60 | #[unsafe(method(acceptsFirstMouse:))] 61 | fn accept_first_mouse(&self, _event: &NSEvent) -> Bool { 62 | self.ivars().accept_first_mouse 63 | } 64 | 65 | #[cfg(target_os = "ios")] 66 | #[unsafe(method_id(inputAccessoryView))] 67 | fn input_accessory_view(&self) -> Option> { 68 | if let Some(builder) = &self.ivars().input_accessory_view_builder { 69 | builder(self) 70 | } else { 71 | unsafe { objc2::msg_send![super(self), inputAccessoryView] } 72 | } 73 | } 74 | } 75 | unsafe impl NSObjectProtocol for WryWebView {} 76 | 77 | // Drag & Drop 78 | #[cfg(target_os = "macos")] 79 | unsafe impl NSDraggingDestination for WryWebView { 80 | #[unsafe(method(draggingEntered:))] 81 | fn dragging_entered( 82 | &self, 83 | drag_info: &ProtocolObject, 84 | ) -> objc2_app_kit::NSDragOperation { 85 | drag_drop::dragging_entered(self, drag_info) 86 | } 87 | 88 | #[unsafe(method(draggingUpdated:))] 89 | fn dragging_updated( 90 | &self, 91 | drag_info: &ProtocolObject, 92 | ) -> objc2_app_kit::NSDragOperation { 93 | drag_drop::dragging_updated(self, drag_info) 94 | } 95 | 96 | #[unsafe(method(performDragOperation:))] 97 | fn perform_drag_operation( 98 | &self, 99 | drag_info: &ProtocolObject, 100 | ) -> Bool { 101 | drag_drop::perform_drag_operation(self, drag_info) 102 | } 103 | 104 | #[unsafe(method(draggingExited:))] 105 | fn dragging_exited(&self, drag_info: &ProtocolObject) { 106 | drag_drop::dragging_exited(self, drag_info) 107 | } 108 | } 109 | 110 | // Synthetic mouse events 111 | #[cfg(target_os = "macos")] 112 | impl WryWebView { 113 | #[unsafe(method(otherMouseDown:))] 114 | fn other_mouse_down(&self, event: &NSEvent) { 115 | synthetic_mouse_events::other_mouse_down(self, event) 116 | } 117 | 118 | #[unsafe(method(otherMouseUp:))] 119 | fn other_mouse_up(&self, event: &NSEvent) { 120 | synthetic_mouse_events::other_mouse_up(self, event) 121 | } 122 | } 123 | ); 124 | 125 | // Custom Protocol Task Checker 126 | impl WryWebView { 127 | pub(crate) fn add_custom_task_key(&self, task_id: usize) -> Retained { 128 | let task_uuid = NSUUID::new(); 129 | self 130 | .ivars() 131 | .custom_protocol_task_ids 132 | .lock() 133 | .unwrap() 134 | .insert(task_id, task_uuid.clone()); 135 | task_uuid 136 | } 137 | pub(crate) fn remove_custom_task_key(&self, task_id: usize) { 138 | self 139 | .ivars() 140 | .custom_protocol_task_ids 141 | .lock() 142 | .unwrap() 143 | .remove(&task_id); 144 | } 145 | pub(crate) fn get_custom_task_uuid(&self, task_id: usize) -> Option> { 146 | self 147 | .ivars() 148 | .custom_protocol_task_ids 149 | .lock() 150 | .unwrap() 151 | .get(&task_id) 152 | .cloned() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /examples/multiwebview.rs: -------------------------------------------------------------------------------- 1 | use winit::{ 2 | application::ApplicationHandler, 3 | event::WindowEvent, 4 | event_loop::{ActiveEventLoop, EventLoop}, 5 | window::{Window, WindowId}, 6 | }; 7 | use wry::{ 8 | dpi::{LogicalPosition, LogicalSize}, 9 | Rect, WebViewBuilder, 10 | }; 11 | 12 | #[derive(Default)] 13 | struct State { 14 | window: Option, 15 | webview1: Option, 16 | webview2: Option, 17 | webview3: Option, 18 | webview4: Option, 19 | } 20 | 21 | impl ApplicationHandler for State { 22 | fn resumed(&mut self, event_loop: &ActiveEventLoop) { 23 | let mut attributes = Window::default_attributes(); 24 | attributes.inner_size = Some(LogicalSize::new(800, 800).into()); 25 | let window = event_loop.create_window(attributes).unwrap(); 26 | 27 | let size = window.inner_size().to_logical::(window.scale_factor()); 28 | 29 | let webview1 = WebViewBuilder::new() 30 | .with_bounds(Rect { 31 | position: LogicalPosition::new(0, 0).into(), 32 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 33 | }) 34 | .with_url("https://tauri.app") 35 | .build_as_child(&window) 36 | .unwrap(); 37 | 38 | let webview2 = WebViewBuilder::new() 39 | .with_bounds(Rect { 40 | position: LogicalPosition::new(size.width / 2, 0).into(), 41 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 42 | }) 43 | .with_url("https://github.com/tauri-apps/wry") 44 | .build_as_child(&window) 45 | .unwrap(); 46 | 47 | let webview3 = WebViewBuilder::new() 48 | .with_bounds(Rect { 49 | position: LogicalPosition::new(0, size.height / 2).into(), 50 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 51 | }) 52 | .with_url("https://twitter.com/TauriApps") 53 | .build_as_child(&window) 54 | .unwrap(); 55 | 56 | let webview4 = WebViewBuilder::new() 57 | .with_bounds(Rect { 58 | position: LogicalPosition::new(size.width / 2, size.height / 2).into(), 59 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 60 | }) 61 | .with_url("https://google.com") 62 | .build_as_child(&window) 63 | .unwrap(); 64 | 65 | self.window = Some(window); 66 | self.webview1 = Some(webview1); 67 | self.webview2 = Some(webview2); 68 | self.webview3 = Some(webview3); 69 | self.webview4 = Some(webview4); 70 | } 71 | 72 | fn window_event( 73 | &mut self, 74 | _event_loop: &ActiveEventLoop, 75 | _window_id: WindowId, 76 | event: WindowEvent, 77 | ) { 78 | match event { 79 | WindowEvent::Resized(size) => { 80 | if let (Some(window), Some(webview1), Some(webview2), Some(webview3), Some(webview4)) = ( 81 | &self.window, 82 | &self.webview1, 83 | &self.webview2, 84 | &self.webview3, 85 | &self.webview4, 86 | ) { 87 | let size = size.to_logical::(window.scale_factor()); 88 | 89 | webview1 90 | .set_bounds(Rect { 91 | position: LogicalPosition::new(0, 0).into(), 92 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 93 | }) 94 | .unwrap(); 95 | 96 | webview2 97 | .set_bounds(Rect { 98 | position: LogicalPosition::new(size.width / 2, 0).into(), 99 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 100 | }) 101 | .unwrap(); 102 | 103 | webview3 104 | .set_bounds(Rect { 105 | position: LogicalPosition::new(0, size.height / 2).into(), 106 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 107 | }) 108 | .unwrap(); 109 | 110 | webview4 111 | .set_bounds(Rect { 112 | position: LogicalPosition::new(size.width / 2, size.height / 2).into(), 113 | size: LogicalSize::new(size.width / 2, size.height / 2).into(), 114 | }) 115 | .unwrap(); 116 | } 117 | } 118 | WindowEvent::CloseRequested => { 119 | std::process::exit(0); 120 | } 121 | _ => {} 122 | } 123 | } 124 | 125 | fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { 126 | #[cfg(any( 127 | target_os = "linux", 128 | target_os = "dragonfly", 129 | target_os = "freebsd", 130 | target_os = "netbsd", 131 | target_os = "openbsd", 132 | ))] 133 | { 134 | while gtk::events_pending() { 135 | gtk::main_iteration_do(false); 136 | } 137 | } 138 | } 139 | } 140 | 141 | fn main() -> wry::Result<()> { 142 | #[cfg(any( 143 | target_os = "linux", 144 | target_os = "dragonfly", 145 | target_os = "freebsd", 146 | target_os = "netbsd", 147 | target_os = "openbsd", 148 | ))] 149 | { 150 | use gtk::prelude::DisplayExtManual; 151 | 152 | gtk::init()?; 153 | if gtk::gdk::Display::default().unwrap().backend().is_wayland() { 154 | panic!("This example doesn't support wayland!"); 155 | } 156 | 157 | winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| { 158 | let error = error as *mut x11_dl::xlib::XErrorEvent; 159 | (unsafe { (*error).error_code }) == 170 160 | })); 161 | } 162 | 163 | let event_loop = EventLoop::new().unwrap(); 164 | let mut state = State::default(); 165 | event_loop.run_app(&mut state).unwrap(); 166 | 167 | Ok(()) 168 | } 169 | -------------------------------------------------------------------------------- /src/webkitgtk/synthetic_mouse_events.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use gtk::{ 4 | gdk::{EventButton, EventMask, ModifierType}, 5 | prelude::*, 6 | }; 7 | use webkit2gtk::{WebView, WebViewExt}; 8 | 9 | pub fn setup(webview: &WebView) { 10 | webview.add_events(EventMask::BUTTON1_MOTION_MASK | EventMask::BUTTON_PRESS_MASK); 11 | 12 | let bf_state = BackForwardState(Rc::new(RefCell::new(0))); 13 | 14 | let bf_state_c = bf_state.clone(); 15 | webview.connect_button_press_event(move |webview, event| { 16 | let mut inhibit = false; 17 | match event.button() { 18 | // back button 19 | 8 => { 20 | inhibit = true; 21 | bf_state_c.set(BACK); 22 | webview.run_javascript( 23 | &create_js_mouse_event(event, true, &bf_state_c), 24 | None::<>k::gio::Cancellable>, 25 | |_| {}, 26 | ); 27 | } 28 | // forward button 29 | 9 => { 30 | inhibit = true; 31 | bf_state_c.set(FORWARD); 32 | webview.run_javascript( 33 | &create_js_mouse_event(event, true, &bf_state_c), 34 | None::<>k::gio::Cancellable>, 35 | |_| {}, 36 | ); 37 | } 38 | _ => {} 39 | } 40 | 41 | if inhibit { 42 | gtk::glib::Propagation::Stop 43 | } else { 44 | gtk::glib::Propagation::Proceed 45 | } 46 | }); 47 | 48 | let bf_state_c = bf_state.clone(); 49 | webview.connect_button_release_event(move |webview, event| { 50 | let mut inhibit = false; 51 | match event.button() { 52 | // back button 53 | 8 => { 54 | inhibit = true; 55 | bf_state_c.remove(BACK); 56 | webview.run_javascript( 57 | &create_js_mouse_event(event, false, &bf_state_c), 58 | None::<>k::gio::Cancellable>, 59 | |_| {}, 60 | ); 61 | } 62 | // forward button 63 | 9 => { 64 | inhibit = true; 65 | bf_state_c.remove(FORWARD); 66 | webview.run_javascript( 67 | &create_js_mouse_event(event, false, &bf_state_c), 68 | None::<>k::gio::Cancellable>, 69 | |_| {}, 70 | ); 71 | } 72 | _ => {} 73 | } 74 | if inhibit { 75 | gtk::glib::Propagation::Stop 76 | } else { 77 | gtk::glib::Propagation::Proceed 78 | } 79 | }); 80 | } 81 | 82 | fn create_js_mouse_event(event: &EventButton, pressed: bool, state: &BackForwardState) -> String { 83 | let event_name = if pressed { "mousedown" } else { "mouseup" }; 84 | // js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button 85 | let button = if event.button() == 8 { 3 } else { 4 }; 86 | let (x, y) = event.position(); 87 | let (x, y) = (x as i32, y as i32); 88 | let modifers_state = event.state(); 89 | let mut buttons = 0; 90 | // left button 91 | if modifers_state.contains(ModifierType::BUTTON1_MASK) { 92 | buttons += 1; 93 | } 94 | // right button 95 | if modifers_state.contains(ModifierType::BUTTON3_MASK) { 96 | buttons += 2; 97 | } 98 | // middle button 99 | if modifers_state.contains(ModifierType::BUTTON2_MASK) { 100 | buttons += 4; 101 | } 102 | // back button 103 | if state.has(BACK) { 104 | buttons += 8; 105 | } 106 | // if modifers_state.contains(ModifierType::BUTTON4_MASK) { 107 | // buttons += 8; 108 | // } 109 | // forward button 110 | if state.has(FORWARD) { 111 | buttons += 16; 112 | } 113 | // if modifers_state.contains(ModifierType::BUTTON5_MASK) { 114 | // buttons += 16; 115 | // } 116 | format!( 117 | r#"(() => {{ 118 | const el = document.elementFromPoint({x},{y}); 119 | const ev = new MouseEvent('{event_name}', {{ 120 | view: window, 121 | button: {button}, 122 | buttons: {buttons}, 123 | x: {x}, 124 | y: {y}, 125 | bubbles: true, 126 | detail: {detail}, 127 | cancelBubble: false, 128 | cancelable: true, 129 | clientX: {x}, 130 | clientY: {y}, 131 | composed: true, 132 | layerX: {x}, 133 | layerY: {y}, 134 | pageX: {x}, 135 | pageY: {y}, 136 | screenX: window.screenX + {x}, 137 | screenY: window.screenY + {y}, 138 | ctrlKey: {ctrl_key}, 139 | metaKey: {meta_key}, 140 | shiftKey: {shift_key}, 141 | altKey: {alt_key}, 142 | }}); 143 | el.dispatchEvent(ev) 144 | if (!ev.defaultPrevented && "{event_name}" === "mouseup") {{ 145 | if (ev.button === 3) {{ 146 | window.history.back(); 147 | }} 148 | if (ev.button === 4) {{ 149 | window.history.forward(); 150 | }} 151 | }} 152 | }})()"#, 153 | event_name = event_name, 154 | x = x, 155 | y = y, 156 | detail = event.click_count().unwrap_or(1), 157 | ctrl_key = modifers_state.contains(ModifierType::CONTROL_MASK), 158 | alt_key = modifers_state.contains(ModifierType::MOD1_MASK), 159 | shift_key = modifers_state.contains(ModifierType::SHIFT_MASK), 160 | meta_key = modifers_state.contains(ModifierType::SUPER_MASK), 161 | button = button, 162 | buttons = buttons, 163 | ) 164 | } 165 | 166 | // Internal modifiers to track whether BACK/FORWARD buttons are pressed 167 | const BACK: u8 = 0b01; 168 | const FORWARD: u8 = 0b10; 169 | 170 | /// A single u8 that stores whether [BACK] and [FORWARD] are pressed or not 171 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 172 | struct BackForwardState(Rc>); 173 | 174 | impl BackForwardState { 175 | fn set(&self, button: u8) { 176 | *self.0.borrow_mut() |= button 177 | } 178 | 179 | fn remove(&self, button: u8) { 180 | *self.0.borrow_mut() &= !button 181 | } 182 | 183 | fn has(&self, button: u8) -> bool { 184 | let state = *self.0.borrow(); 185 | state & !button != state 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/wkwebview/class/wry_navigation_delegate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::sync::{Arc, Mutex}; 6 | 7 | use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly}; 8 | use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; 9 | use objc2_web_kit::{ 10 | WKDownload, WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate, 11 | WKNavigationResponse, WKNavigationResponsePolicy, 12 | }; 13 | 14 | #[cfg(target_os = "ios")] 15 | use crate::wkwebview::ios::WKWebView::WKWebView; 16 | #[cfg(target_os = "macos")] 17 | use objc2_web_kit::WKWebView; 18 | 19 | use crate::{ 20 | url_from_webview, 21 | wkwebview::{ 22 | download::{navigation_download_action, navigation_download_response}, 23 | navigation::{ 24 | did_commit_navigation, did_finish_navigation, navigation_policy, navigation_policy_response, 25 | web_content_process_did_terminate, 26 | }, 27 | }, 28 | PageLoadEvent, WryWebView, 29 | }; 30 | 31 | use super::wry_download_delegate::WryDownloadDelegate; 32 | 33 | pub struct WryNavigationDelegateIvars { 34 | pub pending_scripts: Arc>>>, 35 | pub has_download_handler: bool, 36 | pub navigation_policy_function: Box bool>, 37 | pub download_delegate: Option>, 38 | pub on_page_load_handler: Option>, 39 | pub on_web_content_process_terminate_handler: Option>, 40 | } 41 | 42 | define_class!( 43 | #[unsafe(super(NSObject))] 44 | #[name = "WryNavigationDelegate"] 45 | #[thread_kind = MainThreadOnly] 46 | #[ivars = WryNavigationDelegateIvars] 47 | pub struct WryNavigationDelegate; 48 | 49 | unsafe impl NSObjectProtocol for WryNavigationDelegate {} 50 | 51 | unsafe impl WKNavigationDelegate for WryNavigationDelegate { 52 | #[unsafe(method(webView:decidePolicyForNavigationAction:decisionHandler:))] 53 | fn navigation_policy( 54 | &self, 55 | webview: &WKWebView, 56 | action: &WKNavigationAction, 57 | handler: &block2::Block, 58 | ) { 59 | navigation_policy(self, webview, action, handler); 60 | } 61 | 62 | #[unsafe(method(webView:decidePolicyForNavigationResponse:decisionHandler:))] 63 | fn navigation_policy_response( 64 | &self, 65 | webview: &WKWebView, 66 | response: &WKNavigationResponse, 67 | handler: &block2::Block, 68 | ) { 69 | navigation_policy_response(self, webview, response, handler); 70 | } 71 | 72 | #[unsafe(method(webView:didFinishNavigation:))] 73 | fn did_finish_navigation(&self, webview: &WKWebView, navigation: &WKNavigation) { 74 | did_finish_navigation(self, webview, navigation); 75 | } 76 | 77 | #[unsafe(method(webView:didCommitNavigation:))] 78 | fn did_commit_navigation(&self, webview: &WKWebView, navigation: &WKNavigation) { 79 | did_commit_navigation(self, webview, navigation); 80 | } 81 | 82 | #[unsafe(method(webView:navigationAction:didBecomeDownload:))] 83 | fn navigation_download_action( 84 | &self, 85 | webview: &WKWebView, 86 | action: &WKNavigationAction, 87 | download: &WKDownload, 88 | ) { 89 | navigation_download_action(self, webview, action, download); 90 | } 91 | 92 | #[unsafe(method(webView:navigationResponse:didBecomeDownload:))] 93 | fn navigation_download_response( 94 | &self, 95 | webview: &WKWebView, 96 | response: &WKNavigationResponse, 97 | download: &WKDownload, 98 | ) { 99 | navigation_download_response(self, webview, response, download); 100 | } 101 | 102 | #[unsafe(method(webViewWebContentProcessDidTerminate:))] 103 | fn web_content_process_did_terminate(&self, webview: &WKWebView) { 104 | web_content_process_did_terminate(self, webview); 105 | } 106 | } 107 | ); 108 | 109 | impl WryNavigationDelegate { 110 | #[allow(clippy::too_many_arguments)] 111 | pub fn new( 112 | webview: Retained, 113 | pending_scripts: Arc>>>, 114 | has_download_handler: bool, 115 | navigation_handler: Option bool>>, 116 | download_delegate: Option>, 117 | on_page_load_handler: Option>, 118 | on_web_content_process_terminate_handler: Option>, 119 | mtm: MainThreadMarker, 120 | ) -> Retained { 121 | let navigation_policy_function = Box::new(move |url: String| -> bool { 122 | navigation_handler 123 | .as_ref() 124 | .map_or(true, |navigation_handler| (navigation_handler)(url)) 125 | }); 126 | 127 | let on_page_load_handler = if let Some(handler) = on_page_load_handler { 128 | let custom_handler = Box::new(move |event| { 129 | handler(event, url_from_webview(&webview).unwrap_or_default()); 130 | }) as Box; 131 | Some(custom_handler) 132 | } else { 133 | None 134 | }; 135 | 136 | let on_web_content_process_terminate_handler = 137 | if let Some(handler) = on_web_content_process_terminate_handler { 138 | let custom_handler = Box::new(move || { 139 | handler(); 140 | }) as Box; 141 | Some(custom_handler) 142 | } else { 143 | None 144 | }; 145 | 146 | let delegate = mtm 147 | .alloc::() 148 | .set_ivars(WryNavigationDelegateIvars { 149 | pending_scripts, 150 | navigation_policy_function, 151 | has_download_handler, 152 | download_delegate, 153 | on_page_load_handler, 154 | on_web_content_process_terminate_handler, 155 | }); 156 | 157 | unsafe { msg_send![super(delegate), init] } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | workspace = {} 2 | 3 | [package] 4 | name = "wry" 5 | version = "0.53.5" 6 | authors = ["Tauri Programme within The Commons Conservancy"] 7 | edition = "2021" 8 | license = "Apache-2.0 OR MIT" 9 | description = "Cross-platform WebView rendering library" 10 | readme = "README.md" 11 | repository = "https://github.com/tauri-apps/wry" 12 | documentation = "https://docs.rs/wry" 13 | categories = ["gui"] 14 | rust-version = "1.77" 15 | exclude = ["/.changes", "/.github", "/audits", "/wry-logo.svg"] 16 | 17 | [package.metadata.docs.rs] 18 | no-default-features = true 19 | features = ["drag-drop", "protocol", "os-webview"] 20 | targets = [ 21 | "x86_64-unknown-linux-gnu", 22 | "x86_64-pc-windows-msvc", 23 | "x86_64-apple-darwin", 24 | ] 25 | 26 | [features] 27 | default = ["drag-drop", "protocol", "os-webview", "x11"] 28 | serde = ["dpi/serde"] 29 | drag-drop = [] 30 | protocol = [] 31 | devtools = [] 32 | transparent = [] 33 | fullscreen = [] 34 | linux-body = ["webkit2gtk/v2_40", "os-webview"] 35 | mac-proxy = [] 36 | os-webview = [ 37 | "javascriptcore-rs", 38 | "webkit2gtk", 39 | "webkit2gtk-sys", 40 | "dep:gtk", 41 | "soup3", 42 | ] 43 | x11 = ["x11-dl", "gdkx11", "tao/x11"] 44 | tracing = ["dep:tracing"] 45 | 46 | [dependencies] 47 | tracing = { version = "0.1", optional = true } 48 | once_cell = "1" 49 | thiserror = "2.0" 50 | http = "1.1" 51 | raw-window-handle = { version = "0.6", features = ["std"] } 52 | dpi = "0.1" 53 | cookie = "0.18" 54 | 55 | [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] 56 | javascriptcore-rs = { version = "=1.1.2", features = [ 57 | "v2_28", 58 | ], optional = true } 59 | webkit2gtk = { version = "=2.0.1", features = ["v2_38"], optional = true } 60 | webkit2gtk-sys = { version = "=2.0.1", optional = true } 61 | gtk = { version = "0.18", optional = true } 62 | soup3 = { version = "0.5", optional = true } 63 | x11-dl = { version = "2.21", optional = true } 64 | gdkx11 = { version = "0.18", optional = true } 65 | percent-encoding = "2.3" 66 | dirs = "6" 67 | 68 | [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dev-dependencies] 69 | x11-dl = { version = "2.21" } 70 | 71 | [target."cfg(target_os = \"windows\")".dependencies] 72 | webview2-com = "0.38" 73 | windows-version = "0.1" 74 | windows-core = "0.61" 75 | dunce = "1" 76 | 77 | [target."cfg(target_os = \"windows\")".dependencies.windows] 78 | version = "0.61" 79 | features = [ 80 | "Win32_Foundation", 81 | "Win32_Graphics_Gdi", 82 | "Win32_System_Com", 83 | "Win32_System_Com_StructuredStorage", 84 | "Win32_System_LibraryLoader", 85 | "Win32_System_Ole", 86 | "Win32_System_SystemInformation", 87 | "Win32_System_SystemServices", 88 | "Win32_UI_Shell", 89 | "Win32_UI_WindowsAndMessaging", 90 | "Win32_Globalization", 91 | "Win32_UI_HiDpi", 92 | "Win32_UI_Input", 93 | "Win32_UI_Input_KeyboardAndMouse", 94 | ] 95 | 96 | [target.'cfg(target_vendor = "apple")'.dependencies] 97 | url = "2.5" 98 | dirs = "6" 99 | block2 = "0.6" 100 | objc2 = { version = "0.6", features = [ 101 | "exception", 102 | # because `NSUUID::from_bytes` needs it, 103 | # and `WebViewBuilderExtDarwin.with_data_store_identifier` crashes otherwise, 104 | # see https://github.com/tauri-apps/tauri/issues/12843 105 | "disable-encoding-assertions", 106 | ] } 107 | objc2-web-kit = { version = "0.3.0", default-features = false, features = [ 108 | "std", 109 | "objc2-core-foundation", 110 | "objc2-app-kit", 111 | "block2", 112 | "WKWebView", 113 | "WKWebViewConfiguration", 114 | "WKWebsiteDataStore", 115 | "WKDownload", 116 | "WKDownloadDelegate", 117 | "WKNavigation", 118 | "WKNavigationDelegate", 119 | "WKUserContentController", 120 | "WKURLSchemeHandler", 121 | "WKPreferences", 122 | "WKURLSchemeTask", 123 | "WKScriptMessageHandler", 124 | "WKUIDelegate", 125 | "WKOpenPanelParameters", 126 | "WKFrameInfo", 127 | "WKSecurityOrigin", 128 | "WKScriptMessage", 129 | "WKNavigationAction", 130 | "WKWebpagePreferences", 131 | "WKNavigationResponse", 132 | "WKUserScript", 133 | "WKHTTPCookieStore", 134 | "WKWindowFeatures", 135 | ] } 136 | objc2-core-foundation = { version = "0.3.0", default-features = false, features = [ 137 | "std", 138 | "CFCGTypes", 139 | ] } 140 | objc2-foundation = { version = "0.3.0", default-features = false, features = [ 141 | "std", 142 | "objc2-core-foundation", 143 | "NSURLRequest", 144 | "NSURL", 145 | "NSString", 146 | "NSKeyValueCoding", 147 | "NSStream", 148 | "NSDictionary", 149 | "NSObject", 150 | "NSData", 151 | "NSEnumerator", 152 | "NSKeyValueObserving", 153 | "NSThread", 154 | "NSJSONSerialization", 155 | "NSDate", 156 | "NSBundle", 157 | "NSProcessInfo", 158 | "NSValue", 159 | "NSRange", 160 | "NSRunLoop", 161 | ] } 162 | 163 | [target.'cfg(target_os = "ios")'.dependencies] 164 | objc2-ui-kit = { version = "0.3.0", default-features = false, features = [ 165 | "std", 166 | "objc2-core-foundation", 167 | "UIResponder", 168 | "UIScrollView", 169 | "UIView", 170 | "UIWindow", 171 | "UIApplication", 172 | "UIEvent", 173 | "UIColor", 174 | ] } 175 | 176 | [target.'cfg(target_os = "macos")'.dependencies] 177 | objc2-app-kit = { version = "0.3.0", default-features = false, features = [ 178 | "std", 179 | "objc2-core-foundation", 180 | "NSApplication", 181 | "NSButton", 182 | "NSControl", 183 | "NSEvent", 184 | "NSWindow", 185 | "NSView", 186 | "NSPasteboard", 187 | "NSPanel", 188 | "NSResponder", 189 | "NSOpenPanel", 190 | "NSSavePanel", 191 | "NSMenu", 192 | "NSGraphics", 193 | "NSScreen", 194 | ] } 195 | 196 | [target."cfg(target_os = \"android\")".dependencies] 197 | crossbeam-channel = "0.5" 198 | html5ever = "0.29" 199 | kuchiki = { package = "kuchikiki", version = "=0.8.8-speedreader" } 200 | sha2 = "0.10" 201 | base64 = "0.22" 202 | jni = "0.21" 203 | ndk = "0.9" 204 | tao-macros = "0.1" 205 | libc = "0.2" 206 | 207 | [dev-dependencies] 208 | pollster = "0.4.0" 209 | tao = "0.34" 210 | wgpu = "23" 211 | winit = "0.30" 212 | getrandom = "0.3" 213 | http-range = "0.1" 214 | percent-encoding = "2.3" 215 | 216 | [lints.rust.unexpected_cfgs] 217 | level = "warn" 218 | check-cfg = ["cfg(linux)", "cfg(gtk)"] 219 | -------------------------------------------------------------------------------- /bench/src/utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use anyhow::Result; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use std::{ 9 | collections::HashMap, 10 | fs, 11 | io::{BufRead, BufReader}, 12 | path::PathBuf, 13 | process::{Command, Output, Stdio}, 14 | }; 15 | 16 | #[derive(Default, Clone, Serialize, Deserialize, Debug)] 17 | pub struct BenchResult { 18 | pub created_at: String, 19 | pub sha1: String, 20 | pub exec_time: HashMap>, 21 | pub binary_size: HashMap, 22 | pub max_memory: HashMap, 23 | pub thread_count: HashMap, 24 | pub syscall_count: HashMap, 25 | pub cargo_deps: HashMap, 26 | } 27 | 28 | #[allow(dead_code)] 29 | #[derive(Debug, Clone, Serialize)] 30 | pub struct StraceOutput { 31 | pub percent_time: f64, 32 | pub seconds: f64, 33 | pub usecs_per_call: Option, 34 | pub calls: u64, 35 | pub errors: u64, 36 | } 37 | 38 | pub fn get_target() -> &'static str { 39 | #[cfg(target_os = "macos")] 40 | return "x86_64-apple-darwin"; 41 | #[cfg(target_os = "linux")] 42 | return "x86_64-unknown-linux-gnu"; 43 | #[cfg(target_os = "windows")] 44 | return unimplemented!(); 45 | } 46 | 47 | pub fn target_dir() -> PathBuf { 48 | bench_root_path() 49 | .join("tests") 50 | .join("target") 51 | .join(get_target()) 52 | .join("release") 53 | } 54 | 55 | pub fn bench_root_path() -> PathBuf { 56 | PathBuf::from(env!("CARGO_MANIFEST_DIR")) 57 | } 58 | 59 | #[allow(dead_code)] 60 | pub fn wry_root_path() -> PathBuf { 61 | bench_root_path().parent().unwrap().to_path_buf() 62 | } 63 | 64 | #[allow(dead_code)] 65 | pub fn run_collect(cmd: &[&str]) -> (String, String) { 66 | let mut process_builder = Command::new(cmd[0]); 67 | process_builder 68 | .args(&cmd[1..]) 69 | .stdin(Stdio::piped()) 70 | .stdout(Stdio::piped()) 71 | .stderr(Stdio::piped()); 72 | let prog = process_builder.spawn().expect("failed to spawn script"); 73 | let Output { 74 | stdout, 75 | stderr, 76 | status, 77 | } = prog.wait_with_output().expect("failed to wait on child"); 78 | let stdout = String::from_utf8(stdout).unwrap(); 79 | let stderr = String::from_utf8(stderr).unwrap(); 80 | if !status.success() { 81 | eprintln!("stdout: <<<{}>>>", stdout); 82 | eprintln!("stderr: <<<{}>>>", stderr); 83 | panic!("Unexpected exit code: {:?}", status.code()); 84 | } 85 | (stdout, stderr) 86 | } 87 | 88 | #[allow(dead_code)] 89 | pub fn parse_max_mem(file_path: &str) -> Option { 90 | let file = fs::File::open(file_path).unwrap(); 91 | let output = BufReader::new(file); 92 | let mut highest: u64 = 0; 93 | // MEM 203.437500 1621617192.4123 94 | for line in output.lines().flatten() { 95 | // split line by space 96 | let split = line.split(' ').collect::>(); 97 | if split.len() == 3 { 98 | // mprof generate result in MB 99 | let current_bytes = str::parse::(split[1]).unwrap() as u64 * 1024 * 1024; 100 | if current_bytes > highest { 101 | highest = current_bytes; 102 | } 103 | } 104 | } 105 | 106 | fs::remove_file(file_path).unwrap(); 107 | 108 | if highest > 0 { 109 | return Some(highest); 110 | } 111 | 112 | None 113 | } 114 | 115 | #[allow(dead_code)] 116 | pub fn parse_strace_output(output: &str) -> HashMap { 117 | let mut summary = HashMap::new(); 118 | 119 | let mut lines = output 120 | .lines() 121 | .filter(|line| !line.is_empty() && !line.contains("detached ...")); 122 | let count = lines.clone().count(); 123 | 124 | if count < 4 { 125 | return summary; 126 | } 127 | 128 | let total_line = lines.next_back().unwrap(); 129 | lines.next_back(); // Drop separator 130 | let data_lines = lines.skip(2); 131 | 132 | for line in data_lines { 133 | let syscall_fields = line.split_whitespace().collect::>(); 134 | let len = syscall_fields.len(); 135 | let syscall_name = syscall_fields.last().unwrap(); 136 | 137 | if (5..=6).contains(&len) { 138 | summary.insert( 139 | syscall_name.to_string(), 140 | StraceOutput { 141 | percent_time: str::parse::(syscall_fields[0]).unwrap(), 142 | seconds: str::parse::(syscall_fields[1]).unwrap(), 143 | usecs_per_call: Some(str::parse::(syscall_fields[2]).unwrap()), 144 | calls: str::parse::(syscall_fields[3]).unwrap(), 145 | errors: if syscall_fields.len() < 6 { 146 | 0 147 | } else { 148 | str::parse::(syscall_fields[4]).unwrap() 149 | }, 150 | }, 151 | ); 152 | } 153 | } 154 | 155 | let total_fields = total_line.split_whitespace().collect::>(); 156 | 157 | match total_fields.len() { 158 | // Old format, has no usecs/call 159 | 5 => summary.insert( 160 | "total".to_string(), 161 | StraceOutput { 162 | percent_time: str::parse::(total_fields[0]).unwrap(), 163 | seconds: str::parse::(total_fields[1]).unwrap(), 164 | usecs_per_call: None, 165 | calls: str::parse::(total_fields[2]).unwrap(), 166 | errors: str::parse::(total_fields[3]).unwrap(), 167 | }, 168 | ), 169 | 6 => summary.insert( 170 | "total".to_string(), 171 | StraceOutput { 172 | percent_time: str::parse::(total_fields[0]).unwrap(), 173 | seconds: str::parse::(total_fields[1]).unwrap(), 174 | usecs_per_call: Some(str::parse::(total_fields[2]).unwrap()), 175 | calls: str::parse::(total_fields[3]).unwrap(), 176 | errors: str::parse::(total_fields[4]).unwrap(), 177 | }, 178 | ), 179 | _ => panic!("Unexpected total field count: {}", total_fields.len()), 180 | }; 181 | 182 | summary 183 | } 184 | 185 | #[allow(dead_code)] 186 | pub fn run(cmd: &[&str]) { 187 | let mut process_builder = Command::new(cmd[0]); 188 | process_builder.args(&cmd[1..]).stdin(Stdio::piped()); 189 | let mut prog = process_builder.spawn().expect("failed to spawn script"); 190 | let status = prog.wait().expect("failed to wait on child"); 191 | if !status.success() { 192 | panic!("Unexpected exit code: {:?}", status.code()); 193 | } 194 | } 195 | 196 | #[allow(dead_code)] 197 | pub fn read_json(filename: &str) -> Result { 198 | let f = fs::File::open(filename)?; 199 | Ok(serde_json::from_reader(f)?) 200 | } 201 | 202 | #[allow(dead_code)] 203 | pub fn write_json(filename: &str, value: &Value) -> Result<()> { 204 | let f = fs::File::create(filename)?; 205 | serde_json::to_writer(f, value)?; 206 | Ok(()) 207 | } 208 | -------------------------------------------------------------------------------- /src/webview2/drag_drop.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | // A silly implementation of file drop handling for Windows! 6 | 7 | use crate::DragDropEvent; 8 | 9 | use std::{ 10 | cell::UnsafeCell, 11 | ffi::OsString, 12 | os::{raw::c_void, windows::ffi::OsStringExt}, 13 | path::PathBuf, 14 | ptr, 15 | rc::Rc, 16 | }; 17 | 18 | use windows::{ 19 | core::{implement, BOOL}, 20 | Win32::{ 21 | Foundation::{DRAGDROP_E_INVALIDHWND, HWND, LPARAM, POINT, POINTL}, 22 | Graphics::Gdi::ScreenToClient, 23 | System::{ 24 | Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}, 25 | Ole::{ 26 | IDropTarget, IDropTarget_Impl, RegisterDragDrop, RevokeDragDrop, CF_HDROP, DROPEFFECT, 27 | DROPEFFECT_COPY, DROPEFFECT_NONE, 28 | }, 29 | SystemServices::MODIFIERKEYS_FLAGS, 30 | }, 31 | UI::{ 32 | Shell::{DragFinish, DragQueryFileW, HDROP}, 33 | WindowsAndMessaging::EnumChildWindows, 34 | }, 35 | }, 36 | }; 37 | 38 | #[derive(Default)] 39 | pub(crate) struct DragDropController { 40 | drop_targets: Vec, 41 | } 42 | 43 | impl DragDropController { 44 | #[inline] 45 | pub(crate) fn new(hwnd: HWND, handler: Box bool>) -> Self { 46 | let mut controller = DragDropController::default(); 47 | 48 | let handler = Rc::new(handler); 49 | 50 | // Enumerate child windows to find the WebView2 "window" and override! 51 | { 52 | let mut callback = |hwnd| controller.inject_in_hwnd(hwnd, handler.clone()); 53 | let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback; 54 | let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) }; 55 | let lparam = LPARAM(closure_pointer_pointer as _); 56 | unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL { 57 | let closure = &mut *(lparam.0 as *mut c_void as *mut &mut dyn FnMut(HWND) -> bool); 58 | closure(hwnd).into() 59 | } 60 | let _ = unsafe { EnumChildWindows(Some(hwnd), Some(enumerate_callback), lparam) }; 61 | } 62 | 63 | controller 64 | } 65 | 66 | #[inline] 67 | fn inject_in_hwnd(&mut self, hwnd: HWND, handler: Rc bool>) -> bool { 68 | let drag_drop_target: IDropTarget = DragDropTarget::new(hwnd, handler).into(); 69 | if unsafe { RevokeDragDrop(hwnd) } != Err(DRAGDROP_E_INVALIDHWND.into()) 70 | && unsafe { RegisterDragDrop(hwnd, &drag_drop_target) }.is_ok() 71 | { 72 | self.drop_targets.push(drag_drop_target); 73 | } 74 | 75 | true 76 | } 77 | } 78 | 79 | #[implement(IDropTarget)] 80 | pub struct DragDropTarget { 81 | hwnd: HWND, 82 | listener: Rc bool>, 83 | cursor_effect: UnsafeCell, 84 | enter_is_valid: UnsafeCell, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */ 85 | } 86 | 87 | impl DragDropTarget { 88 | pub fn new(hwnd: HWND, listener: Rc bool>) -> DragDropTarget { 89 | Self { 90 | hwnd, 91 | listener, 92 | cursor_effect: DROPEFFECT_NONE.into(), 93 | enter_is_valid: false.into(), 94 | } 95 | } 96 | 97 | unsafe fn iterate_filenames( 98 | data_obj: windows_core::Ref<'_, IDataObject>, 99 | mut callback: F, 100 | ) -> Option 101 | where 102 | F: FnMut(PathBuf), 103 | { 104 | let drop_format = FORMATETC { 105 | cfFormat: CF_HDROP.0, 106 | ptd: ptr::null_mut(), 107 | dwAspect: DVASPECT_CONTENT.0, 108 | lindex: -1, 109 | tymed: TYMED_HGLOBAL.0 as u32, 110 | }; 111 | 112 | match data_obj 113 | .as_ref() 114 | .expect("Received null IDataObject") 115 | .GetData(&drop_format) 116 | { 117 | Ok(medium) => { 118 | let hdrop = HDROP(medium.u.hGlobal.0 as _); 119 | 120 | // The second parameter (0xFFFFFFFF) instructs the function to return the item count 121 | let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, None); 122 | 123 | for i in 0..item_count { 124 | // Get the length of the path string NOT including the terminating null character. 125 | // Previously, this was using a fixed size array of MAX_PATH length, but the 126 | // Windows API allows longer paths under certain circumstances. 127 | let character_count = DragQueryFileW(hdrop, i, None) as usize; 128 | 129 | // Fill path_buf with the null-terminated file name 130 | let str_len = character_count + 1; 131 | let mut path_buf = vec![0; str_len]; 132 | DragQueryFileW(hdrop, i, Some(&mut path_buf)); 133 | callback(OsString::from_wide(&path_buf[0..character_count]).into()); 134 | } 135 | 136 | Some(hdrop) 137 | } 138 | Err(_error) => { 139 | #[cfg(feature = "tracing")] 140 | tracing::warn!( 141 | "{}", 142 | match _error.code() { 143 | windows::Win32::Foundation::DV_E_FORMATETC => { 144 | // If the dropped item is not a file this error will occur. 145 | // In this case it is OK to return without taking further action. 146 | "Error occurred while processing dropped/hovered item: item is not a file." 147 | } 148 | _ => "Unexpected error occurred while processing dropped/hovered item.", 149 | } 150 | ); 151 | None 152 | } 153 | } 154 | } 155 | } 156 | 157 | #[allow(non_snake_case)] 158 | impl IDropTarget_Impl for DragDropTarget_Impl { 159 | fn DragEnter( 160 | &self, 161 | pDataObj: windows_core::Ref<'_, IDataObject>, 162 | _grfKeyState: MODIFIERKEYS_FLAGS, 163 | pt: &POINTL, 164 | pdwEffect: *mut DROPEFFECT, 165 | ) -> windows::core::Result<()> { 166 | let mut pt = POINT { x: pt.x, y: pt.y }; 167 | let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) }; 168 | 169 | let mut paths = Vec::new(); 170 | let hdrop = unsafe { DragDropTarget::iterate_filenames(pDataObj, |path| paths.push(path)) }; 171 | 172 | let enter_is_valid = hdrop.is_some(); 173 | 174 | if !enter_is_valid { 175 | return Ok(()); 176 | }; 177 | 178 | unsafe { 179 | *self.enter_is_valid.get() = enter_is_valid; 180 | } 181 | 182 | (self.listener)(DragDropEvent::Enter { 183 | paths, 184 | position: (pt.x as _, pt.y as _), 185 | }); 186 | 187 | let cursor_effect = if enter_is_valid { 188 | DROPEFFECT_COPY 189 | } else { 190 | DROPEFFECT_NONE 191 | }; 192 | 193 | unsafe { 194 | *pdwEffect = cursor_effect; 195 | *self.cursor_effect.get() = cursor_effect; 196 | } 197 | 198 | Ok(()) 199 | } 200 | 201 | fn DragOver( 202 | &self, 203 | _grfKeyState: MODIFIERKEYS_FLAGS, 204 | pt: &POINTL, 205 | pdwEffect: *mut DROPEFFECT, 206 | ) -> windows::core::Result<()> { 207 | if unsafe { *self.enter_is_valid.get() } { 208 | let mut pt = POINT { x: pt.x, y: pt.y }; 209 | let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) }; 210 | (self.listener)(DragDropEvent::Over { 211 | position: (pt.x as _, pt.y as _), 212 | }); 213 | } 214 | 215 | unsafe { *pdwEffect = *self.cursor_effect.get() }; 216 | Ok(()) 217 | } 218 | 219 | fn DragLeave(&self) -> windows::core::Result<()> { 220 | if unsafe { *self.enter_is_valid.get() } { 221 | (self.listener)(DragDropEvent::Leave); 222 | } 223 | Ok(()) 224 | } 225 | 226 | fn Drop( 227 | &self, 228 | pDataObj: windows_core::Ref<'_, IDataObject>, 229 | _grfKeyState: MODIFIERKEYS_FLAGS, 230 | pt: &POINTL, 231 | _pdwEffect: *mut DROPEFFECT, 232 | ) -> windows::core::Result<()> { 233 | if unsafe { *self.enter_is_valid.get() } { 234 | let mut pt = POINT { x: pt.x, y: pt.y }; 235 | let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) }; 236 | 237 | let mut paths = Vec::new(); 238 | let hdrop = unsafe { DragDropTarget::iterate_filenames(pDataObj, |path| paths.push(path)) }; 239 | (self.listener)(DragDropEvent::Drop { 240 | paths, 241 | position: (pt.x as _, pt.y as _), 242 | }); 243 | 244 | if let Some(hdrop) = hdrop { 245 | unsafe { DragFinish(hdrop) }; 246 | } 247 | } 248 | 249 | Ok(()) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /examples/wgpu.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, sync::Arc}; 2 | use winit::{ 3 | application::ApplicationHandler, 4 | event::WindowEvent, 5 | event_loop::{ActiveEventLoop, EventLoop}, 6 | window::{Window, WindowId}, 7 | }; 8 | use wry::{ 9 | dpi::{LogicalPosition, LogicalSize}, 10 | Rect, WebViewBuilder, 11 | }; 12 | 13 | #[derive(Default)] 14 | struct State { 15 | window: Option>, 16 | webview: Option, 17 | gfx_state: Option, 18 | } 19 | 20 | struct GfxState { 21 | surface: wgpu::Surface<'static>, 22 | device: wgpu::Device, 23 | queue: wgpu::Queue, 24 | config: wgpu::SurfaceConfiguration, 25 | render_pipeline: wgpu::RenderPipeline, 26 | } 27 | 28 | impl GfxState { 29 | fn new(window: Arc) -> Self { 30 | let instance = wgpu::Instance::default(); 31 | let window_size = window.inner_size(); 32 | let surface = instance.create_surface(window).unwrap(); 33 | 34 | let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { 35 | power_preference: wgpu::PowerPreference::default(), 36 | force_fallback_adapter: false, 37 | compatible_surface: Some(&surface), 38 | })) 39 | .expect("Failed to find an appropriate adapter"); 40 | 41 | let (device, queue) = pollster::block_on(adapter.request_device( 42 | &wgpu::DeviceDescriptor { 43 | label: None, 44 | required_features: wgpu::Features::empty(), 45 | required_limits: 46 | wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()), 47 | memory_hints: wgpu::MemoryHints::Performance, 48 | }, 49 | None, 50 | )) 51 | .expect("Failed to create device"); 52 | 53 | let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 54 | label: None, 55 | source: wgpu::ShaderSource::Wgsl(Cow::Borrowed( 56 | r#" 57 | @vertex 58 | fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4 { 59 | let x = f32(i32(in_vertex_index) - 1); 60 | let y = f32(i32(in_vertex_index & 1u) * 2 - 1); 61 | return vec4(x, y, 0.0, 1.0); 62 | } 63 | 64 | @fragment 65 | fn fs_main() -> @location(0) vec4 { 66 | return vec4(1.0, 0.0, 0.0, 1.0); 67 | } 68 | "#, 69 | )), 70 | }); 71 | 72 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 73 | label: None, 74 | bind_group_layouts: &[], 75 | push_constant_ranges: &[], 76 | }); 77 | 78 | let swapchain_capabilities = surface.get_capabilities(&adapter); 79 | let swapchain_format = swapchain_capabilities.formats[0]; 80 | 81 | let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 82 | label: None, 83 | layout: Some(&pipeline_layout), 84 | vertex: wgpu::VertexState { 85 | module: &shader, 86 | entry_point: Some("vs_main"), 87 | buffers: &[], 88 | compilation_options: Default::default(), 89 | }, 90 | fragment: Some(wgpu::FragmentState { 91 | module: &shader, 92 | entry_point: Some("fs_main"), 93 | targets: &[Some(swapchain_format.into())], 94 | compilation_options: Default::default(), 95 | }), 96 | primitive: wgpu::PrimitiveState::default(), 97 | depth_stencil: None, 98 | multisample: wgpu::MultisampleState::default(), 99 | multiview: None, 100 | cache: None, 101 | }); 102 | 103 | let config = wgpu::SurfaceConfiguration { 104 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 105 | format: swapchain_format, 106 | width: window_size.width, 107 | height: window_size.height, 108 | present_mode: wgpu::PresentMode::Fifo, 109 | desired_maximum_frame_latency: 2, 110 | alpha_mode: swapchain_capabilities.alpha_modes[0], 111 | view_formats: vec![], 112 | }; 113 | 114 | surface.configure(&device, &config); 115 | 116 | Self { 117 | surface, 118 | device, 119 | queue, 120 | config, 121 | render_pipeline, 122 | } 123 | } 124 | 125 | fn render(&mut self) { 126 | let frame = self 127 | .surface 128 | .get_current_texture() 129 | .expect("Failed to acquire next swap chain texture"); 130 | let view = frame 131 | .texture 132 | .create_view(&wgpu::TextureViewDescriptor::default()); 133 | let mut encoder = self 134 | .device 135 | .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); 136 | { 137 | let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 138 | label: None, 139 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 140 | view: &view, 141 | resolve_target: None, 142 | ops: wgpu::Operations { 143 | load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), 144 | store: wgpu::StoreOp::Store, 145 | }, 146 | })], 147 | depth_stencil_attachment: None, 148 | timestamp_writes: None, 149 | occlusion_query_set: None, 150 | }); 151 | render_pass.set_pipeline(&self.render_pipeline); 152 | render_pass.draw(0..3, 0..1); 153 | } 154 | 155 | self.queue.submit(Some(encoder.finish())); 156 | frame.present(); 157 | } 158 | } 159 | 160 | impl ApplicationHandler for State { 161 | fn resumed(&mut self, event_loop: &ActiveEventLoop) { 162 | let mut attributes = Window::default_attributes(); 163 | attributes.transparent = true; 164 | #[cfg(windows)] 165 | { 166 | use winit::platform::windows::WindowAttributesExtWindows; 167 | attributes = attributes.with_clip_children(false); 168 | } 169 | 170 | let window = Arc::new(event_loop.create_window(attributes).unwrap()); 171 | 172 | let gfx_state = GfxState::new(Arc::clone(&window)); 173 | 174 | let webview = WebViewBuilder::new() 175 | .with_bounds(Rect { 176 | position: LogicalPosition::new(100, 100).into(), 177 | size: LogicalSize::new(200, 200).into(), 178 | }) 179 | .with_transparent(true) 180 | .with_html( 181 | r#" 182 | 183 | 188 | "#, 189 | ) 190 | .build_as_child(&window) 191 | .unwrap(); 192 | 193 | window.request_redraw(); 194 | 195 | self.window = Some(window); 196 | self.webview = Some(webview); 197 | self.gfx_state = Some(gfx_state); 198 | } 199 | 200 | fn window_event( 201 | &mut self, 202 | _event_loop: &ActiveEventLoop, 203 | _window_id: WindowId, 204 | event: WindowEvent, 205 | ) { 206 | match event { 207 | WindowEvent::Resized(size) => { 208 | if let Some(gfx_state) = &mut self.gfx_state { 209 | gfx_state.config.width = size.width; 210 | gfx_state.config.height = size.height; 211 | gfx_state 212 | .surface 213 | .configure(&gfx_state.device, &gfx_state.config); 214 | 215 | if let Some(window) = &self.window { 216 | window.request_redraw(); 217 | } 218 | } 219 | } 220 | WindowEvent::RedrawRequested => { 221 | if let Some(gfx_state) = &mut self.gfx_state { 222 | gfx_state.render(); 223 | } 224 | } 225 | WindowEvent::CloseRequested => { 226 | std::process::exit(0); 227 | } 228 | _ => {} 229 | } 230 | } 231 | 232 | fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { 233 | #[cfg(any( 234 | target_os = "linux", 235 | target_os = "dragonfly", 236 | target_os = "freebsd", 237 | target_os = "netbsd", 238 | target_os = "openbsd", 239 | ))] 240 | { 241 | while gtk::events_pending() { 242 | gtk::main_iteration_do(false); 243 | } 244 | } 245 | } 246 | } 247 | 248 | fn main() { 249 | #[cfg(any( 250 | target_os = "linux", 251 | target_os = "dragonfly", 252 | target_os = "freebsd", 253 | target_os = "netbsd", 254 | target_os = "openbsd", 255 | ))] 256 | { 257 | use gtk::prelude::DisplayExtManual; 258 | 259 | gtk::init().unwrap(); 260 | if gtk::gdk::Display::default().unwrap().backend().is_wayland() { 261 | panic!("This example doesn't support wayland!"); 262 | } 263 | 264 | winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| { 265 | let error = error as *mut x11_dl::xlib::XErrorEvent; 266 | (unsafe { (*error).error_code }) == 170 267 | })); 268 | } 269 | 270 | let event_loop = EventLoop::new().unwrap(); 271 | let mut state = State::default(); 272 | event_loop.run_app(&mut state).unwrap(); 273 | } 274 | -------------------------------------------------------------------------------- /bench/src/run_benchmark.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use anyhow::Result; 6 | use std::{ 7 | collections::{HashMap, HashSet}, 8 | env, 9 | path::Path, 10 | process::{Command, Stdio}, 11 | }; 12 | 13 | mod utils; 14 | 15 | /// The list of the examples of the benchmark name and binary relative path 16 | fn get_all_benchmarks() -> Vec<(String, String)> { 17 | vec![ 18 | ( 19 | "wry_hello_world".into(), 20 | format!( 21 | "tests/target/{}/release/bench_hello_world", 22 | utils::get_target() 23 | ), 24 | ), 25 | ( 26 | "wry_custom_protocol".into(), 27 | format!( 28 | "tests/target/{}/release/bench_custom_protocol", 29 | utils::get_target() 30 | ), 31 | ), 32 | ( 33 | "wry_cpu_intensive".into(), 34 | format!( 35 | "tests/target/{}/release/bench_cpu_intensive", 36 | utils::get_target() 37 | ), 38 | ), 39 | ] 40 | } 41 | 42 | fn run_strace_benchmarks(new_data: &mut utils::BenchResult) -> Result<()> { 43 | use std::io::Read; 44 | 45 | let mut thread_count = HashMap::::new(); 46 | let mut syscall_count = HashMap::::new(); 47 | 48 | for (name, example_exe) in get_all_benchmarks() { 49 | let mut file = tempfile::NamedTempFile::new()?; 50 | 51 | Command::new("strace") 52 | .args([ 53 | "-c", 54 | "-f", 55 | "-o", 56 | file.path().to_str().unwrap(), 57 | utils::bench_root_path().join(example_exe).to_str().unwrap(), 58 | ]) 59 | .stdout(Stdio::inherit()) 60 | .spawn()? 61 | .wait()?; 62 | 63 | let mut output = String::new(); 64 | file.as_file_mut().read_to_string(&mut output)?; 65 | 66 | let strace_result = utils::parse_strace_output(&output); 67 | let clone = 1 68 | + strace_result.get("clone").map(|d| d.calls).unwrap_or(0) 69 | + strace_result.get("clone3").map(|d| d.calls).unwrap_or(0); 70 | let total = strace_result.get("total").unwrap().calls; 71 | thread_count.insert(name.to_string(), clone); 72 | syscall_count.insert(name.to_string(), total); 73 | } 74 | 75 | new_data.thread_count = thread_count; 76 | new_data.syscall_count = syscall_count; 77 | 78 | Ok(()) 79 | } 80 | 81 | fn run_max_mem_benchmark() -> Result> { 82 | let mut results = HashMap::::new(); 83 | 84 | for (name, example_exe) in get_all_benchmarks() { 85 | let benchmark_file = utils::target_dir().join(format!("mprof{}_.dat", name)); 86 | let benchmark_file = benchmark_file.to_str().unwrap(); 87 | 88 | let proc = Command::new("mprof") 89 | .args([ 90 | "run", 91 | "-C", 92 | "-o", 93 | benchmark_file, 94 | utils::bench_root_path().join(example_exe).to_str().unwrap(), 95 | ]) 96 | .stdout(Stdio::null()) 97 | .stderr(Stdio::piped()) 98 | .spawn()?; 99 | 100 | let proc_result = proc.wait_with_output()?; 101 | println!("{:?}", proc_result); 102 | results.insert( 103 | name.to_string(), 104 | utils::parse_max_mem(benchmark_file).unwrap(), 105 | ); 106 | } 107 | 108 | Ok(results) 109 | } 110 | 111 | fn rlib_size(target_dir: &std::path::Path, prefix: &str) -> u64 { 112 | let mut size = 0; 113 | let mut seen = std::collections::HashSet::new(); 114 | println!("{:?}", target_dir); 115 | 116 | for entry in std::fs::read_dir(target_dir.join("deps")).unwrap() { 117 | let entry = entry.unwrap(); 118 | let os_str = entry.file_name(); 119 | let name = os_str.to_str().unwrap(); 120 | if name.starts_with(prefix) && name.ends_with(".rlib") { 121 | let start = name.split('-').next().unwrap().to_string(); 122 | if seen.contains(&start) { 123 | println!("skip {}", name); 124 | } else { 125 | seen.insert(start); 126 | size += entry.metadata().unwrap().len(); 127 | println!("check size {} {}", name, size); 128 | } 129 | } 130 | } 131 | assert!(size > 0); 132 | size 133 | } 134 | 135 | fn get_binary_sizes(target_dir: &Path) -> Result> { 136 | let mut sizes = HashMap::::new(); 137 | 138 | let wry_size = rlib_size(target_dir, "libwry"); 139 | println!("wry {} bytes", wry_size); 140 | sizes.insert("wry_rlib".to_string(), wry_size); 141 | 142 | // add up size for everything in target/release/deps/libtao* 143 | let tao_size = rlib_size(target_dir, "libtao"); 144 | println!("tao {} bytes", tao_size); 145 | sizes.insert("tao_rlib".to_string(), tao_size); 146 | 147 | // add size for all EXEC_TIME_BENCHMARKS 148 | for (name, example_exe) in get_all_benchmarks() { 149 | let meta = std::fs::metadata(example_exe).unwrap(); 150 | sizes.insert(name.to_string(), meta.len()); 151 | } 152 | 153 | Ok(sizes) 154 | } 155 | 156 | /// (target OS, target triple) 157 | const TARGETS: &[(&str, &[&str])] = &[ 158 | ( 159 | "Windows", 160 | &[ 161 | "x86_64-pc-windows-gnu", 162 | "i686-pc-windows-gnu", 163 | "i686-pc-windows-msvc", 164 | "x86_64-pc-windows-msvc", 165 | ], 166 | ), 167 | ( 168 | "Linux", 169 | &[ 170 | "x86_64-unknown-linux-gnu", 171 | "i686-unknown-linux-gnu", 172 | "aarch64-unknown-linux-gnu", 173 | ], 174 | ), 175 | ("macOS", &["x86_64-apple-darwin", "aarch64-apple-darwin"]), 176 | ]; 177 | 178 | fn cargo_deps() -> HashMap { 179 | let mut results = HashMap::new(); 180 | for (os, targets) in TARGETS { 181 | for target in *targets { 182 | let mut cmd = Command::new("cargo"); 183 | cmd.arg("tree"); 184 | cmd.arg("--no-dedupe"); 185 | cmd.args(["--edges", "normal"]); 186 | cmd.args(["--prefix", "none"]); 187 | cmd.args(["--target", target]); 188 | cmd.current_dir(&utils::wry_root_path()); 189 | 190 | let full_deps = cmd.output().expect("failed to run cargo tree").stdout; 191 | let full_deps = String::from_utf8(full_deps).expect("cargo tree output not utf-8"); 192 | let count = full_deps.lines().collect::>().len() - 1; // output includes wry itself 193 | 194 | // set the count to the highest count seen for this OS 195 | let existing = results.entry(os.to_string()).or_default(); 196 | *existing = count.max(*existing); 197 | assert!(count > 10); // sanity check 198 | } 199 | } 200 | results 201 | } 202 | 203 | const RESULT_KEYS: &[&str] = &["mean", "stddev", "user", "system", "min", "max"]; 204 | 205 | fn run_exec_time(target_dir: &Path) -> Result>> { 206 | let benchmark_file = target_dir.join("hyperfine_results.json"); 207 | let benchmark_file = benchmark_file.to_str().unwrap(); 208 | 209 | let mut command = [ 210 | "hyperfine", 211 | "--export-json", 212 | benchmark_file, 213 | "--warmup", 214 | "3", 215 | ] 216 | .iter() 217 | .map(|s| s.to_string()) 218 | .collect::>(); 219 | 220 | for (_, example_exe) in get_all_benchmarks() { 221 | command.push( 222 | utils::bench_root_path() 223 | .join(example_exe) 224 | .to_str() 225 | .unwrap() 226 | .to_string(), 227 | ); 228 | } 229 | 230 | utils::run(&command.iter().map(|s| s.as_ref()).collect::>()); 231 | 232 | let mut results = HashMap::>::new(); 233 | let hyperfine_results = utils::read_json(benchmark_file)?; 234 | for ((name, _), data) in get_all_benchmarks().iter().zip( 235 | hyperfine_results 236 | .as_object() 237 | .unwrap() 238 | .get("results") 239 | .unwrap() 240 | .as_array() 241 | .unwrap(), 242 | ) { 243 | let data = data.as_object().unwrap().clone(); 244 | results.insert( 245 | name.to_string(), 246 | data 247 | .into_iter() 248 | .filter(|(key, _)| RESULT_KEYS.contains(&key.as_str())) 249 | .map(|(key, val)| (key, val.as_f64().unwrap())) 250 | .collect(), 251 | ); 252 | } 253 | 254 | Ok(results) 255 | } 256 | 257 | fn main() -> Result<()> { 258 | println!("Starting wry benchmark"); 259 | 260 | let target_dir = utils::target_dir(); 261 | env::set_current_dir(utils::bench_root_path())?; 262 | 263 | let format = 264 | time::format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]Z").unwrap(); 265 | let now = time::OffsetDateTime::now_utc(); 266 | let mut new_data = utils::BenchResult { 267 | created_at: now.format(&format).unwrap(), 268 | sha1: utils::run_collect(&["git", "rev-parse", "HEAD"]) 269 | .0 270 | .trim() 271 | .to_string(), 272 | exec_time: run_exec_time(&target_dir)?, 273 | binary_size: get_binary_sizes(&target_dir)?, 274 | cargo_deps: cargo_deps(), 275 | ..Default::default() 276 | }; 277 | 278 | if cfg!(target_os = "linux") { 279 | run_strace_benchmarks(&mut new_data)?; 280 | new_data.max_memory = run_max_mem_benchmark()?; 281 | } 282 | 283 | println!("===== "); 284 | serde_json::to_writer_pretty(std::io::stdout(), &new_data)?; 285 | println!("\n===== "); 286 | 287 | if let Some(filename) = target_dir.join("bench.json").to_str() { 288 | utils::write_json(filename, &serde_json::to_value(&new_data)?)?; 289 | } else { 290 | eprintln!("Cannot write bench.json, path is invalid"); 291 | } 292 | 293 | Ok(()) 294 | } 295 | -------------------------------------------------------------------------------- /examples/streaming.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | fn main() -> wry::Result<()> { 6 | imp::main() 7 | } 8 | 9 | #[cfg(not(feature = "protocol"))] 10 | mod imp { 11 | pub fn main() -> wry::Result<()> { 12 | unimplemented!() 13 | } 14 | } 15 | 16 | #[cfg(feature = "protocol")] 17 | mod imp { 18 | 19 | use std::{ 20 | io::{Read, Seek, SeekFrom, Write}, 21 | path::PathBuf, 22 | }; 23 | 24 | use http::{header, StatusCode}; 25 | use http_range::HttpRange; 26 | use tao::{ 27 | event::{Event, WindowEvent}, 28 | event_loop::{ControlFlow, EventLoop}, 29 | window::WindowBuilder, 30 | }; 31 | use wry::{ 32 | http::{header::*, Request, Response}, 33 | WebViewBuilder, 34 | }; 35 | 36 | pub fn main() -> wry::Result<()> { 37 | let event_loop = EventLoop::new(); 38 | let window = WindowBuilder::new().build(&event_loop).unwrap(); 39 | 40 | let builder = WebViewBuilder::new() 41 | .with_custom_protocol( 42 | "wry".into(), 43 | move |_webview_id, request| match wry_protocol(request) { 44 | Ok(r) => r.map(Into::into), 45 | Err(e) => http::Response::builder() 46 | .header(CONTENT_TYPE, "text/plain") 47 | .status(500) 48 | .body(e.to_string().as_bytes().to_vec()) 49 | .unwrap() 50 | .map(Into::into), 51 | }, 52 | ) 53 | .with_custom_protocol( 54 | "stream".into(), 55 | move |_webview_id, request| match stream_protocol(request) { 56 | Ok(r) => r.map(Into::into), 57 | Err(e) => http::Response::builder() 58 | .header(CONTENT_TYPE, "text/plain") 59 | .status(500) 60 | .body(e.to_string().as_bytes().to_vec()) 61 | .unwrap() 62 | .map(Into::into), 63 | }, 64 | ) 65 | // tell the webview to load the custom protocol 66 | .with_url("wry://localhost"); 67 | 68 | #[cfg(any( 69 | target_os = "windows", 70 | target_os = "macos", 71 | target_os = "ios", 72 | target_os = "android" 73 | ))] 74 | let _webview = builder.build(&window)?; 75 | #[cfg(not(any( 76 | target_os = "windows", 77 | target_os = "macos", 78 | target_os = "ios", 79 | target_os = "android" 80 | )))] 81 | let _webview = { 82 | use tao::platform::unix::WindowExtUnix; 83 | use wry::WebViewBuilderExtUnix; 84 | let vbox = window.default_vbox().unwrap(); 85 | builder.build_gtk(vbox)? 86 | }; 87 | 88 | event_loop.run(move |event, _, control_flow| { 89 | *control_flow = ControlFlow::Wait; 90 | 91 | if let Event::WindowEvent { 92 | event: WindowEvent::CloseRequested, 93 | .. 94 | } = event 95 | { 96 | *control_flow = ControlFlow::Exit 97 | } 98 | }); 99 | } 100 | 101 | fn wry_protocol( 102 | request: Request>, 103 | ) -> Result>, Box> { 104 | let path = request.uri().path(); 105 | // Read the file content from file path 106 | let root = PathBuf::from("examples/streaming"); 107 | let path = if path == "/" { 108 | "index.html" 109 | } else { 110 | // removing leading slash 111 | &path[1..] 112 | }; 113 | let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; 114 | 115 | // Return asset contents and mime types based on file extentions 116 | // If you don't want to do this manually, there are some crates for you. 117 | // Such as `infer` and `mime_guess`. 118 | let mimetype = if path.ends_with(".html") || path == "/" { 119 | "text/html" 120 | } else if path.ends_with(".js") { 121 | "text/javascript" 122 | } else { 123 | unimplemented!(); 124 | }; 125 | 126 | Response::builder() 127 | .header(CONTENT_TYPE, mimetype) 128 | .body(content) 129 | .map_err(Into::into) 130 | } 131 | 132 | fn stream_protocol( 133 | request: http::Request>, 134 | ) -> Result>, Box> { 135 | // skip leading `/` 136 | let path = percent_encoding::percent_decode(&request.uri().path().as_bytes()[1..]) 137 | .decode_utf8_lossy() 138 | .to_string(); 139 | 140 | let mut file = std::fs::File::open(path)?; 141 | 142 | // get file length 143 | let len = { 144 | let old_pos = file.stream_position()?; 145 | let len = file.seek(SeekFrom::End(0))?; 146 | file.seek(SeekFrom::Start(old_pos))?; 147 | len 148 | }; 149 | 150 | let mut resp = Response::builder().header(CONTENT_TYPE, "video/mp4"); 151 | 152 | // if the webview sent a range header, we need to send a 206 in return 153 | // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers. 154 | let http_response = if let Some(range_header) = request.headers().get("range") { 155 | let not_satisfiable = || { 156 | Response::builder() 157 | .status(StatusCode::RANGE_NOT_SATISFIABLE) 158 | .header(header::CONTENT_RANGE, format!("bytes */{len}")) 159 | .body(vec![]) 160 | }; 161 | 162 | // parse range header 163 | let ranges = if let Ok(ranges) = HttpRange::parse(range_header.to_str()?, len) { 164 | ranges 165 | .iter() 166 | // map the output back to spec range , example: 0-499 167 | .map(|r| (r.start, r.start + r.length - 1)) 168 | .collect::>() 169 | } else { 170 | return Ok(not_satisfiable()?); 171 | }; 172 | 173 | /// The Maximum bytes we send in one range 174 | const MAX_LEN: u64 = 1000 * 1024; 175 | 176 | if ranges.len() == 1 { 177 | let &(start, mut end) = ranges.first().unwrap(); 178 | 179 | // check if a range is not satisfiable 180 | // 181 | // this should be already taken care of by HttpRange::parse 182 | // but checking here again for extra assurance 183 | if start >= len || end >= len || end < start { 184 | return Ok(not_satisfiable()?); 185 | } 186 | 187 | // adjust end byte for MAX_LEN 188 | end = start + (end - start).min(len - start).min(MAX_LEN - 1); 189 | 190 | // calculate number of bytes needed to be read 191 | let bytes_to_read = end + 1 - start; 192 | 193 | // allocate a buf with a suitable capacity 194 | let mut buf = Vec::with_capacity(bytes_to_read as usize); 195 | // seek the file to the starting byte 196 | file.seek(SeekFrom::Start(start))?; 197 | // read the needed bytes 198 | file.take(bytes_to_read).read_to_end(&mut buf)?; 199 | 200 | resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); 201 | resp = resp.header(CONTENT_LENGTH, end + 1 - start); 202 | resp = resp.status(StatusCode::PARTIAL_CONTENT); 203 | resp.body(buf) 204 | } else { 205 | let mut buf = Vec::new(); 206 | let ranges = ranges 207 | .iter() 208 | .filter_map(|&(start, mut end)| { 209 | // filter out unsatisfiable ranges 210 | // 211 | // this should be already taken care of by HttpRange::parse 212 | // but checking here again for extra assurance 213 | if start >= len || end >= len || end < start { 214 | None 215 | } else { 216 | // adjust end byte for MAX_LEN 217 | end = start + (end - start).min(len - start).min(MAX_LEN - 1); 218 | Some((start, end)) 219 | } 220 | }) 221 | .collect::>(); 222 | 223 | let boundary = random_boundary(); 224 | let boundary_sep = format!("\r\n--{boundary}\r\n"); 225 | let boundary_closer = format!("\r\n--{boundary}\r\n"); 226 | 227 | resp = resp.header( 228 | CONTENT_TYPE, 229 | format!("multipart/byteranges; boundary={boundary}"), 230 | ); 231 | 232 | for (end, start) in ranges { 233 | // a new range is being written, write the range boundary 234 | buf.write_all(boundary_sep.as_bytes())?; 235 | 236 | // write the needed headers `Content-Type` and `Content-Range` 237 | buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())?; 238 | buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?; 239 | 240 | // write the separator to indicate the start of the range body 241 | buf.write_all("\r\n".as_bytes())?; 242 | 243 | // calculate number of bytes needed to be read 244 | let bytes_to_read = end + 1 - start; 245 | 246 | let mut local_buf = vec![0_u8; bytes_to_read as usize]; 247 | file.seek(SeekFrom::Start(start))?; 248 | file.read_exact(&mut local_buf)?; 249 | buf.extend_from_slice(&local_buf); 250 | } 251 | // all ranges have been written, write the closing boundary 252 | buf.write_all(boundary_closer.as_bytes())?; 253 | 254 | resp.body(buf) 255 | } 256 | } else { 257 | resp = resp.header(CONTENT_LENGTH, len); 258 | let mut buf = Vec::with_capacity(len as usize); 259 | file.read_to_end(&mut buf)?; 260 | resp.body(buf) 261 | }; 262 | 263 | http_response.map_err(Into::into) 264 | } 265 | 266 | fn random_boundary() -> String { 267 | let mut x = [0_u8; 30]; 268 | getrandom::fill(&mut x).expect("failed to get random bytes"); 269 | (x[..]) 270 | .iter() 271 | .map(|&x| format!("{x:x}")) 272 | .fold(String::new(), |mut a, x| { 273 | a.push_str(x.as_str()); 274 | a 275 | }) 276 | } 277 | } 278 | --------------------------------------------------------------------------------