diff --git a/.changes/rwh.md b/.changes/rwh.md new file mode 100644 index 000000000..b234e348b --- /dev/null +++ b/.changes/rwh.md @@ -0,0 +1,17 @@ +--- +"wry": minor +--- + +Refactor new method to take raw window handle instead. Following are APIs got affected: + - `application` module is removed, and `webivew` module is moved to root module. + - `WebviewBuilder::new`, `Webview::new` now take `RawWindowHandle` instead. + - Attributes `ipc_handler`, `file_drop_handler`, `document_change_handler` don't have window parameter anymore. + Users should use closure to capture the types they want to use. + - Position field in `FileDrop` event is now `Position` instead of `PhysicalPosition`. Users need to handle scale factor + depend on the situation they have. + - `Webview::inner_size` is removed. + +This also means that we removed `tao` as a dependency completely which required some changes to the Android backend: + - We exposed the `android_setup` function that needs to be called once to setup necessary logic. + - Previously the `android_binding!` had internal call to `tao::android_binding` but now that `tao` has been removed,sa + the macro signature has changed and you now need to call `tao::android_binding` yourself, checkout the crate documentation for more information. \ No newline at end of file diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index c105d2ed6..fd10e86e6 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -1,20 +1,33 @@ -name: Audit +# Copyright 2022-2022 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: audit on: workflow_dispatch: schedule: - cron: '0 0 * * *' push: + branches: + - dev + paths: + - 'Cargo.lock' + - 'Cargo.toml' + pull_request: paths: - - "Cargo.lock" - - "Cargo.toml" + - 'Cargo.lock' + - 'Cargo.toml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - audit-rust: + audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: rust audit - uses: actions-rs/audit-check@v1 + - uses: actions/checkout@v4 + - uses: rustsec/audit-check@v1 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 907d11471..7115ec73c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,6 @@ jobs: strategy: fail-fast: false matrix: - rust_version: [stable] platform: - { target: x86_64-pc-windows-msvc, os: windows-latest } - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest } @@ -28,7 +27,7 @@ jobs: if: contains(matrix.platform.target, 'gnu') run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.1 libgtksourceview-3.0-dev libayatana-appindicator3-dev + sudo apt-get install -y libwebkit2gtk-4.1-dev - name: install webview2 (windows only) if: contains(matrix.platform.target, 'windows') @@ -44,20 +43,10 @@ jobs: - name: build tests and examples shell: bash - if: ( - !contains(matrix.platform.target, 'android') && - !contains(matrix.platform.target, 'ios')) + if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios')) run: cargo test --no-run --verbose --target ${{ matrix.platform.target }} - name: run tests - if: ( - !contains(matrix.platform.target, 'android') && - !contains(matrix.platform.target, 'ios')) + if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios')) run: cargo test --verbose --target ${{ matrix.platform.target }} --features linux-body - - name: build wry with winit - if: ( - contains(matrix.platform.target, 'gnu') || - contains(matrix.platform.target, 'windows') || - contains(matrix.platform.target, 'apple')) - run: cargo build --no-default-features --features winit --target ${{ matrix.platform.target }} diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml new file mode 100644 index 000000000..216ab24f3 --- /dev/null +++ b/.github/workflows/clippy-fmt.yml @@ -0,0 +1,48 @@ +# Copyright 2022-2022 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: clippy & fmt + +on: + push: + branches: + - dev + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + clippy: + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4 + - name: install system deps + if: matrix.platform == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - run: cargo clippy --all-targets + + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - run: cargo fmt --all -- --check diff --git a/.github/workflows/covector-status.yml b/.github/workflows/covector-status.yml index 72f9bee13..8cc1b544f 100644 --- a/.github/workflows/covector-status.yml +++ b/.github/workflows/covector-status.yml @@ -6,9 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: covector status uses: jbolda/covector/packages/action@covector-v0 id: covector diff --git a/.github/workflows/covector-version-or-publish.yml b/.github/workflows/covector-version-or-publish.yml index 32d2ef3a3..20d6b7c19 100644 --- a/.github/workflows/covector-version-or-publish.yml +++ b/.github/workflows/covector-version-or-publish.yml @@ -1,4 +1,4 @@ -name: version or publish +name: covector version or publish on: push: @@ -15,15 +15,16 @@ jobs: successfulPublish: ${{ steps.covector.outputs.successfulPublish }} steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 + - name: cargo login run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }} + - name: git config run: | git config --global user.name "${{ github.event.pusher.name }}" git config --global user.email "${{ github.event.pusher.email }}" + - name: covector version or publish (publish when no change files present) uses: jbolda/covector/packages/action@covector-v0 id: covector @@ -34,14 +35,15 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} command: 'version-or-publish' createRelease: true + - name: Create Pull Request With Versions Bumped id: cpr uses: tauri-apps/create-pull-request@v3 if: steps.covector.outputs.commandRan == 'version' with: token: ${{ secrets.GITHUB_TOKEN }} - title: "Publish New Versions" - commit-message: "publish new versions" - labels: "version updates" - branch: "release" + title: Apply Version Updates From Current Changes + commit-message: 'apply version updates' + labels: 'version updates' + branch: 'release' body: ${{ steps.covector.outputs.change }} diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml deleted file mode 100644 index 01ee2187e..000000000 --- a/.github/workflows/fmt.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: fmt check - -on: - pull_request: - paths: - - 'src/**' - - 'Cargo.toml' - -jobs: - clippy_fmt_check: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt,clippy - - uses: Swatinem/rust-cache@v2 - - name: fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check - - name: clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all --examples -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index d5f5bbbcb..c9db01a10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,71 +1,67 @@ -workspace = { } +workspace = {} [package] name = "wry" version = "0.34.2" -authors = [ "Tauri Programme within The Commons Conservancy" ] +authors = ["Tauri Programme within The Commons Conservancy"] edition = "2021" license = "Apache-2.0 OR MIT" description = "Cross-platform WebView rendering library" readme = "README.md" repository = "https://github.com/tauri-apps/wry" documentation = "https://docs.rs/wry" -categories = [ "gui" ] +categories = ["gui"] [package.metadata.docs.rs] no-default-features = true -features = [ "tao", "file-drop", "protocol" ] +features = ["file-drop", "protocol"] targets = [ "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", - "x86_64-apple-darwin" + "x86_64-apple-darwin", ] -rustc-args = [ "--cfg", "docsrs" ] -rustdoc-args = [ "--cfg", "docsrs" ] +rustc-args = ["--cfg", "docsrs"] +rustdoc-args = ["--cfg", "docsrs"] [features] -default = [ "file-drop", "objc-exception", "protocol", "tao" ] -objc-exception = [ "objc/exception" ] -file-drop = [ ] -protocol = [ ] -devtools = [ ] -transparent = [ ] -fullscreen = [ ] -linux-body = [ "webkit2gtk/v2_40" ] -mac-proxy = [ ] +default = ["file-drop", "objc-exception", "protocol"] +objc-exception = ["objc/exception"] +file-drop = [] +protocol = [] +devtools = [] +transparent = [] +fullscreen = [] +linux-body = ["webkit2gtk/v2_40"] +mac-proxy = [] [dependencies] libc = "0.2" log = "0.4" once_cell = "1" -serde = { version = "1.0", features = [ "derive" ] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" url = "2.4" -tao = { version = "0.23", default-features = false, features = [ "serde" ], optional = true } -http = "0.2.9" - -[dev-dependencies] -http-range = "0.1.5" -base64 = "0.21" +http = "0.2" +raw-window-handle = { version = "0.5", features = ["std"] } [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] -javascriptcore-rs = { version = "=1.1", features = [ "v2_28" ] } -webkit2gtk = { version = "=2.0", features = [ "v2_38" ] } +javascriptcore-rs = { version = "=1.1", features = ["v2_28"] } +webkit2gtk = { version = "=2.0", features = ["v2_38"] } webkit2gtk-sys = "=2.0" gtk = "0.18" soup3 = "0.5" -winit = { version = "0.29", package = "winit-gtk", features = [ "serde" ], optional = true } +x11-dl = "2.9" +gdkx11 = "0.18" [target."cfg(target_os = \"windows\")".dependencies] webview2-com = "0.27" windows-implement = "0.51" dunce = "1" -winit = { version = "0.28", features = [ "serde" ], optional = true } - [target."cfg(target_os = \"windows\")".dependencies.windows] - version = "0.51" - features = [ +[target."cfg(target_os = \"windows\")".dependencies.windows] +version = "0.51" +features = [ "implement", "Win32_Foundation", "Win32_Graphics_Gdi", @@ -80,7 +76,7 @@ winit = { version = "0.28", features = [ "serde" ], optional = true } "Win32_Globalization", "Win32_UI_HiDpi", "Win32_UI_Input", - "Win32_UI_Input_KeyboardAndMouse" + "Win32_UI_Input_KeyboardAndMouse", ] [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies] @@ -89,7 +85,6 @@ cocoa = "0.25" core-graphics = "0.23" objc = "0.2" objc_id = "0.1" -winit = { version = "0.28", features = [ "serde" ], optional = true } [target."cfg(target_os = \"android\")".dependencies] crossbeam-channel = "0.5" @@ -97,3 +92,15 @@ html5ever = "0.26" kuchiki = { package = "kuchikiki", version = "0.8" } sha2 = "0.10" base64 = "0.21" +jni = "0.21" +ndk = "0.7" +ndk-sys = "0.4" +ndk-context = "0.1" +tao-macros = { version = "0.1.0" } + +[dev-dependencies] +http-range = "0.1.5" +pollster = "0.3.0" +tao = { git = "https://github.com/tauri-apps/tao", default-features = false, features = ["rwh_05"] } +wgpu = "0.18.0" +winit = { version = "0.29", features = ["rwh_05"] } diff --git a/README.md b/README.md index 838017b01..effe2fa45 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,34 @@ WebView2 provided by Microsoft Edge Chromium is used. So wry supports Windows 7, Wry supports mobile with the help of [`cargo-mobile2`](https://github.com/tauri-apps/cargo-mobile2) CLI to create template project. If you are interested in playing or hacking it, please follow [MOBILE.md](MOBILE.md). -If you wish to create Android project yourself, there are a few kotlin files that are needed to run wry on Android and you have to set the following environment variables: +If you wish to create Android project yourself, there is a few requirements that your application needs to uphold: + +1. You need to set a few environment variables that will be used to generate the necessary kotlin + files that you need to include in your Android application for wry to function properly: + + - `WRY_ANDROID_PACKAGE`: which is the reversed domain name of your android project and the app name in snake_case, for example, `com.wry.example.wry_app` + - `WRY_ANDROID_LIBRARY`: for example, if your cargo project has a lib name `wry_app`, it will generate `libwry_app.so` so you se this env var to `wry_app` + - `WRY_ANDROID_KOTLIN_FILES_OUT_DIR`: for example, `path/to/app/src/main/kotlin/com/wry/example` + +2. Your main Android Activity needs to inherit `AppCompatActivity`, preferably it should use the generated `WryActivity` or inherit it. +3. Your Rust app needs to call `wry::android_setup` function to setup the necessary logic to be able to create webviews later on. +4. Your Rust app needs to call `wry::android_binding!` macro to setup the JNI functions that will be called by `WryActivity` and various other places. + +It is recommended to use [`tao`](https://docs.rs/tao/latest/tao/) crate as it provides maximum compatibility with `wry` + +```rs +#[cfg(target_os = "android")] +{ + tao::android_binding!( + com_example, + wry_app, + WryActivity, + wry::android_setup, // pass the wry::android_setup function to tao which will invoke when the event loop is created + _start_app + ); + wry::android_binding!(com_example, ttt); +} +``` - `WRY_ANDROID_PACKAGE` which is the reversed domain name of your android project and the app name in snake_case for example: `com.wry.example.wry_app` - `WRY_ANDROID_LIBRARY` for example: if your cargo project has a lib name `wry_app`, it will generate `libwry_app.so` so you se this env var to `wry_app` diff --git a/build.rs b/build.rs index 82a448513..dece1aa1d 100644 --- a/build.rs +++ b/build.rs @@ -38,7 +38,7 @@ fn main() { }); let kotlin_files_path = - PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("src/webview/android/kotlin"); + PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("src/android/kotlin"); println!("cargo:rerun-if-changed={}", kotlin_files_path.display()); let kotlin_files = fs::read_dir(kotlin_files_path).expect("failed to read kotlin directory"); diff --git a/examples/async_custom_protocol.rs b/examples/async_custom_protocol.rs new file mode 100644 index 000000000..262561a7a --- /dev/null +++ b/examples/async_custom_protocol.rs @@ -0,0 +1,104 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use http::Request; +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use wry::{ + http::{header::CONTENT_TYPE, Response}, + WebViewBuilder, +}; + +fn main() -> wry::Result<()> { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let builder = WebViewBuilder::new(&window); + + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let builder = { + use tao::platform::unix::WindowExtUnix; + let vbox = window.default_vbox().unwrap(); + WebViewBuilder::new_gtk(vbox) + }; + let _webview = builder + .with_asynchronous_custom_protocol("wry".into(), move |request, responder| { + match get_wry_response(request) { + Ok(http_response) => responder.respond(http_response), + Err(e) => responder.respond( + http::Response::builder() + .header(CONTENT_TYPE, "text/plain") + .status(500) + .body(e.to_string().as_bytes().to_vec()) + .unwrap(), + ), + } + }) + // tell the webview to load the custom protocol + .with_url("wry://localhost")? + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit + } + }); +} + +fn get_wry_response( + request: Request>, +) -> Result>, Box> { + let path = request.uri().path(); + // Read the file content from file path + let root = PathBuf::from("examples/custom_protocol"); + let path = if path == "/" { + "index.html" + } else { + // removing leading slash + &path[1..] + }; + let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; + + // Return asset contents and mime types based on file extentions + // If you don't want to do this manually, there are some crates for you. + // Such as `infer` and `mime_guess`. + let mimetype = if path.ends_with(".html") || path == "/" { + "text/html" + } else if path.ends_with(".js") { + "text/javascript" + } else if path.ends_with(".png") { + "image/png" + } else if path.ends_with(".wasm") { + "application/wasm" + } else { + unimplemented!(); + }; + + Response::builder() + .header(CONTENT_TYPE, mimetype) + .body(content) + .map_err(Into::into) +} diff --git a/examples/custom_protocol.rs b/examples/custom_protocol.rs index 352a5d945..58e303245 100644 --- a/examples/custom_protocol.rs +++ b/examples/custom_protocol.rs @@ -2,43 +2,53 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{ - fs::{canonicalize, read}, - path::PathBuf, -}; +use std::path::PathBuf; use http::Request; +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, http::{header::CONTENT_TYPE, Response}, - webview::WebViewBuilder, + WebViewBuilder, }; -const PAGE1_HTML: &[u8] = include_bytes!("custom_protocol_page1.html"); - fn main() -> wry::Result<()> { let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("Custom Protocol") - .build(&event_loop) - .unwrap(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let builder = WebViewBuilder::new(&window); + + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let builder = { + use tao::platform::unix::WindowExtUnix; + let vbox = window.default_vbox().unwrap(); + WebViewBuilder::new_gtk(vbox) + }; - let _webview = WebViewBuilder::new(window) - .unwrap() - .with_asynchronous_custom_protocol("wry".into(), move |request, responder| { + let _webview = builder + .with_custom_protocol("wry".into(), move |request| { match get_wry_response(request) { - Ok(http_response) => responder.respond(http_response), - Err(e) => responder.respond( - http::Response::builder() - .header(CONTENT_TYPE, "text/plain") - .status(500) - .body(e.to_string().as_bytes().to_vec()) - .unwrap(), - ), + Ok(r) => r.map(Into::into), + Err(e) => http::Response::builder() + .header(CONTENT_TYPE, "text/plain") + .status(500) + .body(e.to_string().as_bytes().to_vec()) + .unwrap() + .map(Into::into), } }) // tell the webview to load the custom protocol @@ -48,13 +58,12 @@ fn main() -> wry::Result<()> { event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; - match event { - Event::NewEvents(StartCause::Init) => println!("Wry application started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit } }); } @@ -64,12 +73,14 @@ fn get_wry_response( ) -> Result>, Box> { let path = request.uri().path(); // Read the file content from file path - let content = if path == "/" { - PAGE1_HTML.into() + let root = PathBuf::from("examples/custom_protocol"); + let path = if path == "/" { + "index.html" } else { - // `1..` for removing leading slash - read(canonicalize(PathBuf::from("examples").join(&path[1..]))?)? + // removing leading slash + &path[1..] }; + let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; // Return asset contents and mime types based on file extentions // If you don't want to do this manually, there are some crates for you. diff --git a/examples/custom_protocol/index.html b/examples/custom_protocol/index.html new file mode 100644 index 000000000..1d9022ffb --- /dev/null +++ b/examples/custom_protocol/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + +

Welcome to WRY!

+

Page 1

+ +

+ Link + + + + diff --git a/examples/custom_protocol/script.js b/examples/custom_protocol/script.js new file mode 100644 index 000000000..f6f257dcb --- /dev/null +++ b/examples/custom_protocol/script.js @@ -0,0 +1,22 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +if (window.location.pathname.startsWith("/page2")) { + console.log("hello from javascript in page2"); +} else { + console.log("hello from javascript in page1"); + + if (typeof WebAssembly.instantiateStreaming !== "undefined") { + WebAssembly.instantiateStreaming(fetch("/wasm.wasm")).then((wasm) => { + console.log(wasm.instance.exports.main()); // should log 42 + }); + } else { + // Older WKWebView may not support `WebAssembly.instantiateStreaming` yet. + fetch("/wasm.wasm") + .then((response) => response.arrayBuffer()) + .then((bytes) => WebAssembly.instantiate(bytes)) + .then((wasm) => { + console.log(wasm.instance.exports.main()); // should log 42 + }); + } +} diff --git a/examples/custom_protocol_page2.html b/examples/custom_protocol/subpage.html similarity index 100% rename from examples/custom_protocol_page2.html rename to examples/custom_protocol/subpage.html diff --git a/examples/custom_protocol_wasm.wasm b/examples/custom_protocol/wasm.wasm similarity index 100% rename from examples/custom_protocol_wasm.wasm rename to examples/custom_protocol/wasm.wasm diff --git a/examples/custom_protocol_page1.html b/examples/custom_protocol_page1.html deleted file mode 100644 index 61a86d419..000000000 --- a/examples/custom_protocol_page1.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - -

Welcome to WRY!

-

Page 1

- -

- Link - - - - - diff --git a/examples/custom_protocol_script.js b/examples/custom_protocol_script.js deleted file mode 100644 index ecfd1c137..000000000 --- a/examples/custom_protocol_script.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT -if (window.location.pathname.startsWith('/custom_protocol_page2')) { - console.log("hello from javascript in page2"); -} else { - console.log("hello from javascript in page1"); - - if (typeof WebAssembly.instantiateStreaming !== 'undefined') { - WebAssembly.instantiateStreaming(fetch("/custom_protocol_wasm.wasm")) - .then(wasm => { - console.log(wasm.instance.exports.main()); // should log 42 - }); - } else { - // Older WKWebView may not support `WebAssembly.instantiateStreaming` yet. - fetch("/custom_protocol_wasm.wasm") - .then(response => response.arrayBuffer()) - .then(bytes => WebAssembly.instantiate(bytes)) - .then(wasm => { - console.log(wasm.instance.exports.main()); // should log 42 - }); - } -} diff --git a/examples/custom_titlebar.rs b/examples/custom_titlebar.rs index 8b07a125d..67af0dbde 100644 --- a/examples/custom_titlebar.rs +++ b/examples/custom_titlebar.rs @@ -2,20 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoopBuilder}, - window::{Window, WindowBuilder}, - }, - webview::WebViewBuilder, - }; - - enum UserEvent { - CloseWindow, - } +use tao::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoopBuilder}, + window::WindowBuilder, +}; +use wry::WebViewBuilder; + +enum UserEvent { + Minimize, + Maximize, + DragWindow, + CloseWindow, +} +fn main() -> wry::Result<()> { let event_loop = EventLoopBuilder::::with_user_event().build(); let window = WindowBuilder::new() .with_decorations(false) @@ -101,7 +102,6 @@ fn main() -> wry::Result<()> { e.detail === 2 ? window.ipc.postMessage('maximize') : window.ipc.postMessage('drag_window'); - } }) document.addEventListener('touchstart', (e) => { if (e.target.classList.contains('drag-region')) { @@ -115,25 +115,44 @@ fn main() -> wry::Result<()> { "#; let proxy = event_loop.create_proxy(); - - let handler = move |window: &Window, req: String| { - if req == "minimize" { - window.set_minimized(true); + let handler = move |req: String| match req.as_str() { + "minimize" => { + let _ = proxy.send_event(UserEvent::Minimize); } - if req == "maximize" { - window.set_maximized(!window.is_maximized()); + "maximize" => { + let _ = proxy.send_event(UserEvent::Maximize); } - if req == "close" { - let _ = proxy.send_event(UserEvent::CloseWindow); + "drag_window" => { + let _ = proxy.send_event(UserEvent::DragWindow); } - if req == "drag_window" { - let _ = window.drag_window(); + "close" => { + let _ = proxy.send_event(UserEvent::CloseWindow); } + _ => {} + }; + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let builder = WebViewBuilder::new(&window); + + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let builder = { + use tao::platform::unix::WindowExtUnix; + let vbox = window.default_vbox().unwrap(); + WebViewBuilder::new_gtk(vbox) }; let mut webview = Some( - WebViewBuilder::new(window) - .unwrap() + builder .with_html(HTML)? .with_ipc_handler(handler) .with_accept_first_mouse(true) @@ -153,6 +172,13 @@ fn main() -> wry::Result<()> { let _ = webview.take(); *control_flow = ControlFlow::Exit } + + Event::UserEvent(e) => match e { + UserEvent::Minimize => window.set_minimized(true), + UserEvent::Maximize => window.set_maximized(!window.is_maximized()), + UserEvent::DragWindow => window.drag_window().unwrap(), + UserEvent::CloseWindow => { /* handled above */ } + }, _ => (), } }); diff --git a/examples/detect_js_ecma.rs b/examples/detect_js_ecma.rs deleted file mode 100644 index b5f5f3579..000000000 --- a/examples/detect_js_ecma.rs +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; - - const HTML: &str = r#" - - - -

ECMAScript support list:

-
    - - -

    Details:

    -

    - -
    - - - - - "#; - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("Detect ECMAScript") - .build(&event_loop) - .unwrap(); - let _webview = WebViewBuilder::new(window) - .unwrap() - .with_html(HTML)? - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry application started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} diff --git a/examples/download_event.rs b/examples/download_event.rs deleted file mode 100644 index e2715d1fc..000000000 --- a/examples/download_event.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2019-2021 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use std::{env::temp_dir, path::PathBuf}; - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoopBuilder}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; - - const HTML: &str = r#" - -
    -

    WRYYYYYYYYYYYYYYYYYYYYYY!

    - Allowed Download - Denied Download -
    - - "#; - - enum UserEvent { - DownloadStarted(String, String), - DownloadComplete(Option, bool), - Rejected(String), - } - - let event_loop = EventLoopBuilder::::with_user_event().build(); - let proxy = event_loop.create_proxy(); - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop)?; - let _webview = WebViewBuilder::new(window)? - .with_html(HTML)? - .with_download_started_handler({ - let proxy = proxy.clone(); - move |uri: String, default_path: &mut PathBuf| { - if uri.contains("wry-v0.13.3") { - let path = temp_dir().join("example.zip").as_path().to_path_buf(); - - *default_path = path.clone(); - - let submitted = proxy - .send_event(UserEvent::DownloadStarted(uri, path.display().to_string())) - .is_ok(); - - return submitted; - } - - let _ = proxy.send_event(UserEvent::Rejected(uri)); - - false - } - }) - .with_download_completed_handler({ - let proxy = proxy; - move |_uri, path, success| { - let _ = proxy.send_event(UserEvent::DownloadComplete(path, success)); - } - }) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::UserEvent(UserEvent::DownloadStarted(uri, temp_dir)) => { - println!("Download: {}", uri); - println!("Will write to: {:?}", temp_dir); - } - Event::UserEvent(UserEvent::DownloadComplete(path, success)) => { - let path = path.map(|_| temp_dir().join("example.zip")); - println!("Succeeded: {}", success); - if let Some(path) = path { - println!("Path: {}", path.to_string_lossy()); - let metadata = path.metadata().unwrap(); - println!("Size of {}Mb", (metadata.len() / 1024) / 1024) - } else { - println!("No output path") - } - } - Event::UserEvent(UserEvent::Rejected(uri)) => { - println!("Rejected download from: {}", uri) - } - _ => (), - } - }); -} diff --git a/examples/dragndrop.rs b/examples/dragndrop.rs deleted file mode 100644 index 55d0d8e05..000000000 --- a/examples/dragndrop.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; - - const HTML: &str = r#"data:text/html, -Drop files onto the window and read the console!
    -Dropping files onto the following form is also possible:

    - -"#; - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); - let _webview = WebViewBuilder::new(window) - .unwrap() - .with_url(HTML)? - .with_file_drop_handler(|_, data| { - println!("Window 1: {:?}", data); - false // Returning true will block the OS default behaviour. - }) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry application started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => {} - } - }); -} diff --git a/examples/eval_js.rs b/examples/eval_js.rs deleted file mode 100644 index 6f4bc2299..000000000 --- a/examples/eval_js.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2020-2022 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoopBuilder}, - window::{Window, WindowBuilder}, - }, - webview::WebViewBuilder, - }; - - enum UserEvent { - ExecEval, - } - - let event_loop = EventLoopBuilder::::with_user_event().build(); - let proxy = event_loop.create_proxy(); - - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop)?; - - let ipc_handler = move |_: &Window, req: String| { - if req == "exec-eval" { - let _ = proxy.send_event(UserEvent::ExecEval); - } - }; - - let _webview = WebViewBuilder::new(window)? - .with_html( - r#" - - "#, - )? - .with_ipc_handler(ipc_handler) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::UserEvent(UserEvent::ExecEval) => { - // String - _webview - .evaluate_script_with_callback( - "if (!foo) { var foo = 'morbin'; } `${foo} time`", - |result| println!("String: {:?}", result), - ) - .unwrap(); - - // Number - _webview - .evaluate_script_with_callback("var num = 9527; num", |result| { - println!("Number: {:?}", result) - }) - .unwrap(); - - // Object - _webview - .evaluate_script_with_callback("var obj = { thank: 'you', '95': 27 }; obj", |result| { - println!("Object: {:?}", result) - }) - .unwrap(); - - // Array - _webview - .evaluate_script_with_callback("var ary = [1,2,3,4,'5']; ary", |result| { - println!("Array: {:?}", result) - }) - .unwrap(); - // Exception thrown - _webview - .evaluate_script_with_callback("throw new Error()", |result| { - println!("Exception Occured: {:?}", result) - }) - .unwrap(); - } - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} diff --git a/examples/form.html b/examples/form.html deleted file mode 100644 index 332a639bb..000000000 --- a/examples/form.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - -

    Welcome to WRY!

    -
    -
    -
    -
    -
    -
    -

    - -
    -

    - If you click the "Submit" button, the form-data will be sent to the custom - protocol. -

    - - - \ No newline at end of file diff --git a/examples/form.rs b/examples/form.rs deleted file mode 100644 index 094336923..000000000 --- a/examples/form.rs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{ - borrow::Cow, - fs::{canonicalize, read}, -}; - -use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - http::{header::CONTENT_TYPE, method::Method, Response}, - webview::WebViewBuilder, -}; - -fn main() -> wry::Result<()> { - let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop) - .unwrap(); - - let _webview = WebViewBuilder::new(window) - .unwrap() - .with_custom_protocol("wry".into(), move |request| { - if request.method() == Method::POST { - let body_string = String::from_utf8_lossy(request.body()); - for body in body_string.split('&') { - println!("Value sent; {:?}", body); - } - } - - // remove leading slash - let path = &request.uri().path()[1..]; - - get_response(path).unwrap_or_else(|error| { - http::Response::builder() - .status(http::StatusCode::BAD_REQUEST) - .header(CONTENT_TYPE, "text/plain") - .body(error.to_string().as_bytes().to_vec().into()) - .unwrap() - }) - }) - // tell the webview to load the custom protocol - .with_url("wry://localhost/examples/form.html")? - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry application started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} - -fn get_response(path: &str) -> Result>, Box> { - Response::builder() - .header(CONTENT_TYPE, "text/html") - .body(read(canonicalize(path)?)?.into()) - .map_err(Into::into) -} diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs deleted file mode 100644 index 48abdf6be..000000000 --- a/examples/fullscreen.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::{Fullscreen, WindowBuilder}, - }, - webview::WebViewBuilder, - }; - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("3D Render Test") - .with_fullscreen(Some(Fullscreen::Borderless(None))) - .build(&event_loop) - .unwrap(); - let _webview = WebViewBuilder::new(window) - .unwrap() - .with_url("https://browserbench.org/MotionMark1.2/")? - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => {} - } - }); -} - -// Test Result: -// CPU: i7 9750H || GPU: Intel(R) UHD Graphics 630 -// Linux kernel 5.8.18-18-ibryza-standard-xin -// Mesa Mesa 20.2.6 -// ================================================ -// Canvas score - Test 1: 542 - Test 2: 368 -// WebGL score - Test 1: 1390 - Test 2: 1342 -// Total score: 3642 diff --git a/examples/hello_world.rs b/examples/hello_world.rs deleted file mode 100644 index f74d27625..000000000 --- a/examples/hello_world.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop)?; - let _webview = WebViewBuilder::new(window)? - .with_url("https://www.netflix.com/browse")? - // .with_incognito(true) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} diff --git a/examples/multi_window.rs b/examples/multi_window.rs deleted file mode 100644 index b74ed14f1..000000000 --- a/examples/multi_window.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use std::collections::HashMap; - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, - window::{Window, WindowBuilder, WindowId}, - }, - webview::{WebView, WebViewBuilder}, - }; - - enum UserEvent { - CloseWindow(WindowId), - NewWindow, - } - - fn create_new_window( - title: String, - event_loop: &EventLoopWindowTarget, - proxy: EventLoopProxy, - ) -> (WindowId, WebView) { - let window = WindowBuilder::new() - .with_title(title) - .build(event_loop) - .unwrap(); - let window_id = window.id(); - let handler = move |window: &Window, req: String| match req.as_str() { - "new-window" => { - let _ = proxy.send_event(UserEvent::NewWindow); - } - "close" => { - let _ = proxy.send_event(UserEvent::CloseWindow(window.id())); - } - _ if req.starts_with("change-title") => { - let title = req.replace("change-title:", ""); - window.set_title(title.as_str()); - } - _ => {} - }; - - let webview = WebViewBuilder::new(window) - .unwrap() - .with_html( - r#" - - - - "#, - ) - .unwrap() - .with_ipc_handler(handler) - .build() - .unwrap(); - (window_id, webview) - } - - let event_loop = EventLoopBuilder::::with_user_event().build(); - let mut webviews = HashMap::new(); - let proxy = event_loop.create_proxy(); - - let new_window = create_new_window( - format!("Window {}", webviews.len() + 1), - &event_loop, - proxy.clone(), - ); - webviews.insert(new_window.0, new_window.1); - - event_loop.run(move |event, event_loop, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry application started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - window_id, - .. - } => { - webviews.remove(&window_id); - if webviews.is_empty() { - *control_flow = ControlFlow::Exit - } - } - Event::UserEvent(UserEvent::NewWindow) => { - let new_window = create_new_window( - format!("Window {}", webviews.len() + 1), - event_loop, - proxy.clone(), - ); - webviews.insert(new_window.0, new_window.1); - } - Event::UserEvent(UserEvent::CloseWindow(id)) => { - webviews.remove(&id); - if webviews.is_empty() { - *control_flow = ControlFlow::Exit - } - } - _ => (), - } - }); -} diff --git a/examples/multiwebview.rs b/examples/multiwebview.rs new file mode 100644 index 000000000..4c7ed6957 --- /dev/null +++ b/examples/multiwebview.rs @@ -0,0 +1,106 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use winit::{ + dpi::LogicalSize, + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use wry::WebViewBuilder; + +fn main() -> wry::Result<()> { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + { + use gtk::prelude::DisplayExtManual; + + gtk::init()?; + if gtk::gdk::Display::default().unwrap().backend().is_wayland() { + panic!("This example doesn't support wayland!"); + } + + // we need to ignore this error here otherwise it will be catched by winit and will be + // make the example crash + winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| { + let error = error as *mut x11_dl::xlib::XErrorEvent; + (unsafe { (*error).error_code }) == 170 + })); + } + + let event_loop = EventLoop::new().unwrap(); + let window = WindowBuilder::new() + .with_inner_size(LogicalSize::new(800, 800)) + .build(&event_loop) + .unwrap(); + + let size = window.inner_size().to_logical::(window.scale_factor()); + + let webview = WebViewBuilder::new_as_child(&window) + .with_position((0, 0)) + .with_size((size.width / 2, size.height / 2)) + .with_url("https://tauri.app")? + .build()?; + let webview2 = WebViewBuilder::new_as_child(&window) + .with_position(((size.width / 2) as i32, 0)) + .with_size((size.width / 2, size.height / 2)) + .with_url("https://github.com/tauri-apps/wry")? + .build()?; + let webview3 = WebViewBuilder::new_as_child(&window) + .with_position((0, (size.height / 2) as i32)) + .with_size((size.width / 2, size.height / 2)) + .with_url("https://twitter.com/TauriApps")? + .build()?; + let webview4 = WebViewBuilder::new_as_child(&window) + .with_position(((size.width / 2) as i32, (size.height / 2) as i32)) + .with_size((size.width / 2, size.height / 2)) + .with_url("https://google.com")? + .build()?; + + event_loop + .run(move |event, evl| { + evl.set_control_flow(ControlFlow::Poll); + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + while gtk::events_pending() { + gtk::main_iteration_do(false); + } + + match event { + Event::WindowEvent { + event: WindowEvent::Resized(size), + .. + } => { + let size = size.to_logical::(window.scale_factor()); + webview.set_size((size.width / 2, size.height / 2)); + webview.set_position((0, 0)); + webview2.set_position(((size.width / 2) as i32, 0)); + webview2.set_size((size.width / 2, size.height / 2)); + webview3.set_position((0, (size.height / 2) as i32)); + webview3.set_size((size.width / 2, size.height / 2)); + webview4.set_position(((size.width / 2) as i32, (size.height / 2) as i32)); + webview4.set_size((size.width / 2, size.height / 2)); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => evl.exit(), + _ => {} + } + }) + .unwrap(); + + Ok(()) +} diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs new file mode 100644 index 000000000..bb146d0e8 --- /dev/null +++ b/examples/multiwindow.rs @@ -0,0 +1,125 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowBuilder, WindowId}, +}; +use wry::{WebView, WebViewBuilder}; + +enum UserEvent { + CloseWindow(WindowId), + NewTitle(WindowId, String), + NewWindow, +} + +fn main() -> wry::Result<()> { + let event_loop = EventLoopBuilder::::with_user_event().build(); + let mut webviews = HashMap::new(); + let proxy = event_loop.create_proxy(); + + let new_window = create_new_window( + format!("Window {}", webviews.len() + 1), + &event_loop, + proxy.clone(), + ); + webviews.insert(new_window.0.id(), (new_window.0, new_window.1)); + + event_loop.run(move |event, event_loop, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + .. + } => { + webviews.remove(&window_id); + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } + } + Event::UserEvent(UserEvent::NewWindow) => { + let new_window = create_new_window( + format!("Window {}", webviews.len() + 1), + event_loop, + proxy.clone(), + ); + webviews.insert(new_window.0.id(), (new_window.0, new_window.1)); + } + Event::UserEvent(UserEvent::CloseWindow(id)) => { + webviews.remove(&id); + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } + } + + Event::UserEvent(UserEvent::NewTitle(id, title)) => { + webviews.get(&id).unwrap().0.set_title(&title); + } + _ => (), + } + }); +} + +fn create_new_window( + title: String, + event_loop: &EventLoopWindowTarget, + proxy: EventLoopProxy, +) -> (Window, WebView) { + let window = WindowBuilder::new() + .with_title(title) + .build(event_loop) + .unwrap(); + let window_id = window.id(); + let handler = move |req: String| match req.as_str() { + "new-window" => { + let _ = proxy.send_event(UserEvent::NewWindow); + } + "close" => { + let _ = proxy.send_event(UserEvent::CloseWindow(window_id)); + } + _ if req.starts_with("change-title") => { + let title = req.replace("change-title:", ""); + let _ = proxy.send_event(UserEvent::NewTitle(window_id, title)); + } + _ => {} + }; + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let builder = WebViewBuilder::new(&window); + + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let builder = { + use tao::platform::unix::WindowExtUnix; + let vbox = window.default_vbox().unwrap(); + WebViewBuilder::new_gtk(vbox) + }; + + let webview = builder + .with_html( + r#" + + + + "#, + ) + .unwrap() + .with_ipc_handler(handler) + .build() + .unwrap(); + (window, webview) +} diff --git a/examples/navigation_event.rs b/examples/navigation_event.rs deleted file mode 100644 index ddaab184b..000000000 --- a/examples/navigation_event.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoopBuilder}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; - - enum UserEvent { - Navigation(String), - } - - let event_loop = EventLoopBuilder::::with_user_event().build(); - let proxy = event_loop.create_proxy(); - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop)?; - let _webview = WebViewBuilder::new(window)? - .with_url("http://neverssl.com")? - .with_navigation_handler(move |uri: String| { - let submitted = proxy.send_event(UserEvent::Navigation(uri.clone())).is_ok(); - - submitted && uri.contains("neverssl") - }) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::UserEvent(UserEvent::Navigation(uri)) => { - println!("{}", uri); - } - _ => (), - } - }); -} diff --git a/examples/new_window_req_event.rs b/examples/new_window_req_event.rs deleted file mode 100644 index 272d68d70..000000000 --- a/examples/new_window_req_event.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoopBuilder}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; - - enum UserEvent { - NewWindow(String), - } - - let html = r#" - -
    -

    WRYYYYYYYYYYYYYYYYYYYYYY!

    - Visit Wikipedia - (Try to) visit GitHub -
    - - "#; - - let event_loop = EventLoopBuilder::::with_user_event().build(); - let proxy = event_loop.create_proxy(); - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop)?; - let _webview = WebViewBuilder::new(window)? - .with_html(html)? - .with_new_window_req_handler(move |uri: String| { - let submitted = proxy.send_event(UserEvent::NewWindow(uri.clone())).is_ok(); - - submitted && uri.contains("wikipedia") - }) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::UserEvent(UserEvent::NewWindow(uri)) => { - println!("New Window: {}", uri); - } - _ => (), - } - }); -} diff --git a/examples/proxy.rs b/examples/proxy.rs deleted file mode 100644 index f411b0278..000000000 --- a/examples/proxy.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use wry::webview::{ProxyConfig, ProxyEndpoint}; - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("Proxy Test") - .build(&event_loop)?; - - let http_proxy = ProxyConfig::Http(ProxyEndpoint { - host: "localhost".to_string(), - port: "3128".to_string(), - }); - - let _webview = WebViewBuilder::new(window)? - .with_proxy_config(http_proxy) - .with_url("https://www.myip.com/")? - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 000000000..000b0b752 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,49 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use wry::WebViewBuilder; + +fn main() -> wry::Result<()> { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let builder = WebViewBuilder::new(&window); + + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let builder = { + use tao::platform::unix::WindowExtUnix; + let vbox = window.default_vbox().unwrap(); + WebViewBuilder::new_gtk(vbox) + }; + + let _webview = builder.with_url("https://tauri.app")?.build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit + } + }); +} diff --git a/examples/stream.html b/examples/stream.html deleted file mode 100644 index f3a7d0478..000000000 --- a/examples/stream.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/stream_range.rs b/examples/stream_range.rs deleted file mode 100644 index aa0d5bd75..000000000 --- a/examples/stream_range.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use http_range::HttpRange; -use std::{ - borrow::Cow, - fs::{canonicalize, File}, - io::{Read, Seek, SeekFrom}, - path::PathBuf, - process::{Command, Stdio}, -}; -use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - http::{header::CONTENT_TYPE, status::StatusCode, Response}, - webview::WebViewBuilder, -}; - -fn main() -> wry::Result<()> { - let video_file = PathBuf::from("examples/test_video.mp4"); - let video_url = - "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; - - if !video_file.exists() { - // Downloading with curl this saves us from adding - // a Rust HTTP client dependency. - println!("Downloading {}", video_url); - let status = Command::new("curl") - .arg("-L") - .arg("-o") - .arg(&video_file) - .arg(video_url) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output() - .unwrap(); - - assert!(status.status.success()); - assert!(video_file.exists()); - } - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop) - .unwrap(); - - let _webview = WebViewBuilder::new(window) - .unwrap() - .with_custom_protocol("wry".into(), move |request| { - get_stream_response(request).unwrap_or_else(|error| { - http::Response::builder() - .status(http::StatusCode::BAD_REQUEST) - .header(CONTENT_TYPE, "text/plain") - .body(error.to_string().as_bytes().to_vec().into()) - .unwrap() - }) - }) - // tell the webview to load the custom protocol - .with_url("wry://localhost/examples/stream.html")? - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry application started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => {} - } - }); -} - -fn get_stream_response( - request: http::Request>, -) -> Result>, Box> { - // remove leading slash - let path = &request.uri().path()[1..]; - - // Read the file content from file path - let mut content = File::open(canonicalize(path)?)?; - - // Return asset contents and mime types based on file extentions - // If you don't want to do this manually, there are some crates for you. - // Such as `infer` and `mime_guess`. - let mut status_code = StatusCode::OK; - let mut buf = Vec::new(); - - // guess our mimetype from the path - let mimetype = if path.ends_with(".html") { - "text/html" - } else if path.ends_with(".mp4") { - "video/mp4" - } else { - unimplemented!(); - }; - - // prepare our http response - let mut response = Response::builder(); - - // read our range header if it exist, so we can return partial content - if let Some(range) = request.headers().get("range") { - // Get the file size - let file_size = content.metadata().unwrap().len(); - - // we parse the range header - let range = HttpRange::parse(range.to_str().unwrap(), file_size).unwrap(); - - // let support only 1 range for now - let first_range = range.first(); - if let Some(range) = first_range { - let mut real_length = range.length; - - // prevent max_length; - // specially on webview2 - if range.length > file_size / 3 { - // max size sent (400ko / request) - // as it's local file system we can afford to read more often - real_length = 1024 * 400; - } - - // last byte we are reading, the length of the range include the last byte - // who should be skipped on the header - let last_byte = range.start + real_length - 1; - status_code = StatusCode::PARTIAL_CONTENT; - - response = response.header("Connection", "Keep-Alive"); - response = response.header("Accept-Ranges", "bytes"); - // we need to overwrite our content length - response = response.header("Content-Length", real_length); - response = response.header( - "Content-Range", - format!("bytes {}-{}/{}", range.start, last_byte, file_size), - ); - - // seek our file bytes - content.seek(SeekFrom::Start(range.start))?; - content.take(real_length).read_to_end(&mut buf)?; - } else { - content.read_to_end(&mut buf)?; - } - } else { - content.read_to_end(&mut buf)?; - } - - response - .header(CONTENT_TYPE, mimetype) - .status(status_code) - .body(buf.into()) - .map_err(Into::into) -} diff --git a/examples/transparent.rs b/examples/transparent.rs index 915afc9f2..1182f314a 100644 --- a/examples/transparent.rs +++ b/examples/transparent.rs @@ -2,53 +2,79 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use wry::WebViewBuilder; +fn main() -> wry::Result<()> { let event_loop = EventLoop::new(); - let window = WindowBuilder::new() + #[allow(unused_mut)] + let mut builder = WindowBuilder::new() .with_decorations(false) // There are actually three layer of background color when creating webview window. // The first is window background... - .with_transparent(true) - .build(&event_loop) - .unwrap(); + .with_transparent(true); + #[cfg(target_os = "windows")] + { + use tao::platform::windows::WindowBuilderExtWindows; + builder = builder.with_undecorated_shadow(false); + } + let window = builder.build(&event_loop).unwrap(); + + #[cfg(target_os = "windows")] + { + use tao::platform::windows::WindowExtWindows; + window.set_undecorated_shadow(true); + } + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let builder = WebViewBuilder::new(&window); + + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let builder = { + use tao::platform::unix::WindowExtUnix; + let vbox = window.default_vbox().unwrap(); + WebViewBuilder::new_gtk(vbox) + }; - let _webview = WebViewBuilder::new(window)? + let _webview = builder // The second is on webview... .with_transparent(true) // And the last is in html. .with_html( - r#" - - - hello - - "#, + r#" + + + "#, )? .build()?; event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; - match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => {} + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit } }); } diff --git a/examples/user_agent.rs b/examples/user_agent.rs deleted file mode 100644 index ae45dad2a..000000000 --- a/examples/user_agent.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::{webview_version, WebViewBuilder}, - }; - - let current_version = env!("CARGO_PKG_VERSION"); - let current_webview_version = webview_version().unwrap(); - let user_agent_string = format!( - "wry/{} ({}; {})", - current_version, - std::env::consts::OS, - current_webview_version - ); - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop)?; - let _webview = WebViewBuilder::new(window)? - .with_user_agent(&user_agent_string) - .with_url("https://www.whatismybrowser.com/detect/what-is-my-user-agent")? - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} diff --git a/examples/wgpu.rs b/examples/wgpu.rs new file mode 100644 index 000000000..34a80cf69 --- /dev/null +++ b/examples/wgpu.rs @@ -0,0 +1,218 @@ +use std::borrow::Cow; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::Window, +}; +use wry::WebViewBuilder; + +async fn run(event_loop: EventLoop<()>, window: Window) { + let size = window.inner_size(); + + let instance = wgpu::Instance::default(); + + let surface = unsafe { instance.create_surface(&window) }.unwrap(); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + force_fallback_adapter: false, + // Request an adapter which can render to our surface + compatible_surface: Some(&surface), + }) + .await + .expect("Failed to find an appropriate adapter"); + + // Create the logical device and command queue + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + features: wgpu::Features::empty(), + // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain. + limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()), + }, + None, + ) + .await + .expect("Failed to create device"); + + // Load the shaders from disk + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed( + r#" +@vertex +fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4 { + let x = f32(i32(in_vertex_index) - 1); + let y = f32(i32(in_vertex_index & 1u) * 2 - 1); + return vec4(x, y, 0.0, 1.0); +} + +@fragment +fn fs_main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); +} +"#, + )), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[], + push_constant_ranges: &[], + }); + + let swapchain_capabilities = surface.get_capabilities(&adapter); + let swapchain_format = swapchain_capabilities.formats[0]; + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(swapchain_format.into())], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let mut config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + alpha_mode: swapchain_capabilities.alpha_modes[0], + view_formats: vec![], + }; + + surface.configure(&device, &config); + + let _webview = WebViewBuilder::new_as_child(&window) + .with_position((100, 100)) + .with_size((400, 400)) + .with_transparent(true) + .with_html( + r#" + + + "#, + ) + .unwrap() + .build() + .unwrap(); + + event_loop + .run(move |event, evl| { + evl.set_control_flow(ControlFlow::Poll); + + match event { + Event::WindowEvent { + event: WindowEvent::Resized(size), + .. + } => { + // Reconfigure the surface with the new size + config.width = size.width; + config.height = size.height; + surface.configure(&device, &config); + // On macos the window needs to be redrawn manually after resizing + window.request_redraw(); + } + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { + let frame = surface + .get_current_texture() + .expect("Failed to acquire next swap chain texture"); + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_pipeline(&render_pipeline); + rpass.draw(0..3, 0..1); + } + + queue.submit(Some(encoder.finish())); + frame.present(); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => evl.exit(), + _ => {} + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + while gtk::events_pending() { + gtk::main_iteration_do(false); + } + }) + .unwrap(); +} + +fn main() { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + { + use gtk::prelude::DisplayExtManual; + + gtk::init().unwrap(); + if gtk::gdk::Display::default().unwrap().backend().is_wayland() { + panic!("This example doesn't support wayland!"); + } + + // we need to ignore this error here otherwise it will be catched by winit and will be + // make the example crash + winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| { + let error = error as *mut x11_dl::xlib::XErrorEvent; + (unsafe { (*error).error_code }) == 170 + })); + } + + let event_loop = EventLoop::new().unwrap(); + let window = winit::window::WindowBuilder::new() + .with_transparent(true) + .build(&event_loop) + .unwrap(); + pollster::block_on(run(event_loop, window)); +} diff --git a/examples/winit.rs b/examples/winit.rs new file mode 100644 index 000000000..4ffa79304 --- /dev/null +++ b/examples/winit.rs @@ -0,0 +1,86 @@ +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use winit::{ + dpi::LogicalSize, + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use wry::WebViewBuilder; + +fn main() -> wry::Result<()> { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + { + use gtk::prelude::DisplayExtManual; + + gtk::init().unwrap(); + if gtk::gdk::Display::default().unwrap().backend().is_wayland() { + panic!("This example doesn't support wayland!"); + } + + // we need to ignore this error here otherwise it will be catched by winit and will be + // make the example crash + winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| { + let error = error as *mut x11_dl::xlib::XErrorEvent; + (unsafe { (*error).error_code }) == 170 + })); + } + + let event_loop = EventLoop::new().unwrap(); + let window = WindowBuilder::new() + .with_inner_size(LogicalSize::new(800, 800)) + .build(&event_loop) + .unwrap(); + + #[allow(unused_mut)] + let mut builder = WebViewBuilder::new(&window); + let _webview = builder.with_url("https://tauri.app")?.build()?; + + event_loop + .run(move |event, evl| { + evl.set_control_flow(ControlFlow::Poll); + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + while gtk::events_pending() { + gtk::main_iteration_do(false); + } + + match event { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + Event::WindowEvent { + event: WindowEvent::Resized(size), + .. + } => { + _webview.set_size(size.into()); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => evl.exit(), + _ => {} + } + }) + .unwrap(); + + Ok(()) +} diff --git a/src/webview/android/binding.rs b/src/android/binding.rs similarity index 90% rename from src/webview/android/binding.rs rename to src/android/binding.rs index 72d09a903..15c316387 100644 --- a/src/webview/android/binding.rs +++ b/src/android/binding.rs @@ -6,35 +6,30 @@ use http::{ header::{HeaderName, HeaderValue, CONTENT_TYPE}, Request, }; -pub use tao::platform::android::ndk_glue::jni::sys::{jboolean, jstring}; -use tao::platform::android::ndk_glue::jni::{ - errors::Error as JniError, - objects::{JClass, JMap, JObject, JString}, - sys::jobject, +use jni::errors::Result as JniResult; +pub use jni::{ + self, + objects::{GlobalRef, JClass, JMap, JObject, JString}, + sys::{jboolean, jobject, jstring}, JNIEnv, }; +pub use ndk; use super::{ ASSET_LOADER_DOMAIN, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER, URL_LOADING_OVERRIDE, WITH_ASSET_LOADER, }; -use crate::webview::PageLoadEvent; +use crate::PageLoadEvent; #[macro_export] macro_rules! android_binding { - ($domain:ident, $package:ident, $main:ident) => { - android_binding!($domain, $package, $main, ::wry) + ($domain:ident, $package:ident) => { + ::wry::android_binding!($domain, $package, ::wry) }; - ($domain:ident, $package:ident, $main:ident, $wry:path) => { - use $wry::{ - application::{ - android_binding as tao_android_binding, android_fn, generate_package_name, - platform::android::ndk_glue::*, - }, - webview::prelude::*, - }; - tao_android_binding!($domain, $package, WryActivity, setup, $main); + ($domain:ident, $package:ident, $wry:path) => {{ + use $wry::prelude::*; + android_fn!( $domain, $package, @@ -97,10 +92,10 @@ macro_rules! android_binding { handleReceivedTitle, [JObject, JString], ); - }; + }}; } -fn handle_request(env: &mut JNIEnv, request: JObject) -> Result { +fn handle_request(env: &mut JNIEnv, request: JObject) -> JniResult { if let Some(handler) = REQUEST_HANDLER.get() { let mut request_builder = Request::builder(); @@ -113,10 +108,10 @@ fn handle_request(env: &mut JNIEnv, request: JObject) -> Result { let arg = arg.to_string_lossy().to_string(); if let Some(ipc) = IPC.get() { - (ipc.handler)(&ipc.window, arg) + (ipc.handler)(arg) } } Err(e) => log::warn!("Failed to parse JString: {}", e), @@ -279,7 +274,7 @@ pub unsafe fn handleReceivedTitle(mut env: JNIEnv, _: JClass, _webview: JObject, Ok(title) => { let title = title.to_string_lossy().to_string(); if let Some(title_handler) = TITLE_CHANGE_HANDLER.get() { - (title_handler.handler)(&title_handler.window, title) + (title_handler.handler)(title) } } Err(e) => log::warn!("Failed to parse JString: {}", e), diff --git a/src/webview/android/kotlin/Ipc.kt b/src/android/kotlin/Ipc.kt similarity index 100% rename from src/webview/android/kotlin/Ipc.kt rename to src/android/kotlin/Ipc.kt diff --git a/src/webview/android/kotlin/Logger.kt b/src/android/kotlin/Logger.kt similarity index 100% rename from src/webview/android/kotlin/Logger.kt rename to src/android/kotlin/Logger.kt diff --git a/src/webview/android/kotlin/PermissionHelper.kt b/src/android/kotlin/PermissionHelper.kt similarity index 100% rename from src/webview/android/kotlin/PermissionHelper.kt rename to src/android/kotlin/PermissionHelper.kt diff --git a/src/webview/android/kotlin/RustWebChromeClient.kt b/src/android/kotlin/RustWebChromeClient.kt similarity index 100% rename from src/webview/android/kotlin/RustWebChromeClient.kt rename to src/android/kotlin/RustWebChromeClient.kt diff --git a/src/webview/android/kotlin/RustWebView.kt b/src/android/kotlin/RustWebView.kt similarity index 100% rename from src/webview/android/kotlin/RustWebView.kt rename to src/android/kotlin/RustWebView.kt diff --git a/src/webview/android/kotlin/RustWebViewClient.kt b/src/android/kotlin/RustWebViewClient.kt similarity index 100% rename from src/webview/android/kotlin/RustWebViewClient.kt rename to src/android/kotlin/RustWebViewClient.kt diff --git a/src/webview/android/kotlin/WryActivity.kt b/src/android/kotlin/WryActivity.kt similarity index 100% rename from src/webview/android/kotlin/WryActivity.kt rename to src/android/kotlin/WryActivity.kt diff --git a/src/webview/android/kotlin/proguard-wry.pro b/src/android/kotlin/proguard-wry.pro similarity index 100% rename from src/webview/android/kotlin/proguard-wry.pro rename to src/android/kotlin/proguard-wry.pro diff --git a/src/webview/android/main_pipe.rs b/src/android/main_pipe.rs similarity index 94% rename from src/webview/android/main_pipe.rs rename to src/android/main_pipe.rs index c791f19c8..c934b8736 100644 --- a/src/webview/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -2,20 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::{webview::RGBA, Error}; +use crate::{Error, RGBA}; use crossbeam_channel::*; +use jni::{ + errors::Result as JniResult, + objects::{GlobalRef, JMap, JObject, JString}, + JNIEnv, +}; use once_cell::sync::Lazy; use std::os::unix::prelude::*; -use tao::platform::android::ndk_glue::{ - jni::{ - errors::Error as JniError, - objects::{GlobalRef, JObject, JString}, - JNIEnv, - }, - JMap, PACKAGE, -}; -use super::find_class; +use super::{find_class, PACKAGE}; static CHANNEL: Lazy<(Sender, Receiver)> = Lazy::new(|| bounded(8)); pub static MAIN_PIPE: Lazy<[RawFd; 2]> = Lazy::new(|| { @@ -39,7 +36,7 @@ impl<'a> MainPipe<'a> { } } - pub fn recv(&mut self) -> Result<(), JniError> { + pub fn recv(&mut self) -> JniResult<()> { let activity = self.activity.as_obj(); if let Ok(message) = CHANNEL.1.recv() { match message { @@ -265,7 +262,7 @@ fn load_url<'a>( url: &JString<'a>, headers: Option, main_thread: bool, -) -> Result<(), JniError> { +) -> JniResult<()> { let function = if main_thread { "loadUrlMainThread" } else { @@ -294,11 +291,7 @@ fn load_url<'a>( Ok(()) } -fn load_html<'a>( - env: &mut JNIEnv<'a>, - webview: &JObject<'a>, - html: &JString<'a>, -) -> Result<(), JniError> { +fn load_html<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>, html: &JString<'a>) -> JniResult<()> { env.call_method( webview, "loadHTMLMainThread", @@ -312,7 +305,7 @@ fn set_background_color<'a>( env: &mut JNIEnv<'a>, webview: &JObject<'a>, background_color: RGBA, -) -> Result<(), JniError> { +) -> JniResult<()> { let color_class = env.find_class("android/graphics/Color")?; let color = env.call_static_method( color_class, @@ -349,6 +342,6 @@ pub(crate) struct CreateWebViewAttributes { pub background_color: Option, pub headers: Option, pub autoplay: bool, - pub on_webview_created: Option Result<(), JniError> + Send>>, + pub on_webview_created: Option JniResult<()> + Send>>, pub user_agent: Option, } diff --git a/src/webview/android/mod.rs b/src/android/mod.rs similarity index 89% rename from src/webview/android/mod.rs rename to src/android/mod.rs index bb87d5dbc..1789082c9 100644 --- a/src/webview/android/mod.rs +++ b/src/android/mod.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use super::{PageLoadEvent, WebContext, WebViewAttributes, RGBA}; -use crate::{application::window::Window, webview::RequestAsyncResponder, Result}; +use crate::{RequestAsyncResponder, Result}; use base64::{engine::general_purpose, Engine}; use crossbeam_channel::*; use html5ever::{interface::QualName, namespace_url, ns, tendril::TendrilSink, LocalName}; @@ -11,19 +11,16 @@ use http::{ header::{HeaderValue, CONTENT_SECURITY_POLICY, CONTENT_TYPE}, Request, Response as HttpResponse, }; +use jni::{ + errors::Result as JniResult, + objects::{GlobalRef, JClass, JObject}, + JNIEnv, +}; use kuchiki::NodeRef; +use ndk::looper::{FdEvent, ForeignLooper}; use once_cell::sync::OnceCell; use sha2::{Digest, Sha256}; -use std::{borrow::Cow, rc::Rc, sync::mpsc::channel}; -use tao::platform::android::ndk_glue::{ - jni::{ - errors::Error as JniError, - objects::{GlobalRef, JClass, JObject}, - JNIEnv, - }, - ndk::looper::{FdEvent, ForeignLooper}, - PACKAGE, -}; +use std::{borrow::Cow, sync::mpsc::channel}; use url::Url; pub(crate) mod binding; @@ -55,9 +52,9 @@ macro_rules! define_static_handlers { } define_static_handlers! { - IPC = UnsafeIpc { handler: Box, window: Rc }; + IPC = UnsafeIpc { handler: Box }; REQUEST_HANDLER = UnsafeRequestHandler { handler: Box>) -> Option>>> }; - TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box, window: Rc }; + TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box }; URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box bool> }; ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box }; } @@ -65,7 +62,17 @@ define_static_handlers! { pub static WITH_ASSET_LOADER: OnceCell = OnceCell::new(); pub static ASSET_LOADER_DOMAIN: OnceCell = OnceCell::new(); -pub unsafe fn setup(mut env: JNIEnv, looper: &ForeignLooper, activity: GlobalRef) { +pub(crate) static PACKAGE: OnceCell = OnceCell::new(); + +/// Sets up the necessary logic for wry to be able to create the webviews later. +pub unsafe fn android_setup( + package: &str, + mut env: JNIEnv, + looper: &ForeignLooper, + activity: GlobalRef, +) { + PACKAGE.get_or_init(move || package.to_string()); + // we must create the WebChromeClient here because it calls `registerForActivityResult`, // which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook let rust_webchrome_client_class = find_class( @@ -103,14 +110,20 @@ pub unsafe fn setup(mut env: JNIEnv, looper: &ForeignLooper, activity: GlobalRef .unwrap(); } -pub(crate) struct InnerWebView { - #[allow(unused)] - pub window: Rc, -} +pub(crate) struct InnerWebView; impl InnerWebView { + pub fn new_as_child( + _window: &impl raw_window_handle::HasRawWindowHandle, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + _web_context: Option<&mut WebContext>, + ) -> Result { + Self::new(_window, attributes, pl_attrs, _web_context) + } + pub fn new( - window: Rc, + _window: &impl raw_window_handle::HasRawWindowHandle, attributes: WebViewAttributes, pl_attrs: super::PlatformSpecificWebViewAttributes, _web_context: Option<&mut WebContext>, @@ -247,14 +260,12 @@ impl InnerWebView { })) }); - let w = window.clone(); if let Some(i) = ipc_handler { - IPC.get_or_init(move || UnsafeIpc::new(Box::new(i), w)); + IPC.get_or_init(move || UnsafeIpc::new(Box::new(i))); } - let w = window.clone(); if let Some(i) = attributes.document_title_changed_handler { - TITLE_CHANGE_HANDLER.get_or_init(move || UnsafeTitleHandler::new(i, w)); + TITLE_CHANGE_HANDLER.get_or_init(move || UnsafeTitleHandler::new(i)); } if let Some(i) = attributes.navigation_handler { @@ -265,7 +276,7 @@ impl InnerWebView { ON_LOAD_HANDLER.get_or_init(move || UnsafeOnPageLoadHandler::new(h)); } - Ok(Self { window }) + Ok(Self) } pub fn print(&self) {} @@ -312,6 +323,22 @@ impl InnerWebView { MainPipe::send(WebViewMessage::ClearAllBrowsingData); Ok(()) } + + pub fn set_position(&self, _position: (i32, i32)) { + // Unsupported. + } + + pub fn set_size(&self, _size: (u32, u32)) { + // Unsupported. + } + + pub fn set_visible(&self, _visible: bool) { + // Unsupported + } + + pub fn focus(&self) { + // Unsupported + } } #[derive(Clone, Copy)] @@ -359,7 +386,7 @@ pub fn find_class<'a>( env: &mut JNIEnv<'a>, activity: &JObject<'_>, name: String, -) -> std::result::Result, JniError> { +) -> JniResult> { let class_name = env.new_string(name.replace('/', "."))?; let my_class = env .call_method( diff --git a/src/application.rs b/src/application.rs deleted file mode 100644 index 9f9cf7625..000000000 --- a/src/application.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Re-exported Tao APIs -//! -//! This module re-export [tao] APIs for user to create application windows. To learn more about -//! how to use tao, please see [its documentation](https://crates.io/crates/tao). -//! -//! [tao]: https://crates.io/crates/tao - -#[cfg(feature = "tao")] -pub use tao::*; - -#[cfg(feature = "winit")] -pub use winit::*; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 000000000..65abe92a5 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,91 @@ +/// Convenient type alias of Result type for wry. +pub type Result = std::result::Result; + +/// Errors returned by wry. +#[non_exhaustive] +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[error(transparent)] + GlibError(#[from] gtk::glib::Error), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[error(transparent)] + GlibBoolError(#[from] gtk::glib::BoolError), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[error("Fail to fetch security manager")] + MissingManager, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[error("Couldn't find X11 Display")] + X11DisplayNotFound, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[error(transparent)] + XlibError(#[from] x11_dl::error::OpenError), + #[error("Failed to initialize the script")] + InitScriptError, + #[error("Bad RPC request: {0} ((1))")] + RpcScriptError(String, String), + #[error(transparent)] + NulError(#[from] std::ffi::NulError), + #[error(transparent)] + ReceiverError(#[from] std::sync::mpsc::RecvError), + #[error(transparent)] + SenderError(#[from] std::sync::mpsc::SendError), + #[error("Failed to send the message")] + MessageSender, + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + UrlError(#[from] url::ParseError), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[cfg(target_os = "windows")] + #[error("WebView2 error: {0}")] + WebView2Error(webview2_com::Error), + #[error("Duplicate custom protocol registered: {0}")] + DuplicateCustomProtocol(String), + #[error(transparent)] + HttpError(#[from] http::Error), + #[error("Infallible error, something went really wrong: {0}")] + Infallible(#[from] std::convert::Infallible), + #[cfg(target_os = "android")] + #[error(transparent)] + JniError(#[from] jni::errors::Error), + #[error("Failed to create proxy endpoint")] + ProxyEndpointCreationFailed, + #[error(transparent)] + WindowHandleError(#[from] raw_window_handle::HandleError), + #[error("the window handle kind is not supported")] + UnsupportedWindowHandle, + #[error(transparent)] + Utf8Error(#[from] std::str::Utf8Error), +} diff --git a/src/lib.rs b/src/lib.rs index 6007818ac..98dc32c5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,60 +2,153 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#![cfg_attr(docsrs, feature(doc_cfg))] + //! Wry is a Cross-platform WebView rendering library. //! -//! To build a Window with WebView embedded, we could use [`application`] module to create -//! [`EventLoop`] and the window. It's a module that re-exports APIs from [tao]. Then -//! use [`webview`] module to create the [`WebView`] from the [`Window`]. Here's a minimum example -//! showing how to create a hello world window and load the url to Tauri website. +//! The webview requires a running event loop and a window type that implements [`HasWindowHandle`], +//! or a gtk container widget if you need to support X11 and Wayland. +//! You can use a windowing library like [`tao`] or [`winit`]. +//! +//! ## Examples +//! +//! This example leverages the [`HasWindowHandle`] and supports Windows, macOS, iOS, Android and Linux (X11 Only) +//! +//! ```no_run +//! use wry::WebViewBuilder; +//! +//! # struct T; +//! # unsafe impl raw_window_handle::HasRawWindowHandle for T { +//! # fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { +//! # raw_window_handle::RawWindowHandle::Win32(raw_window_handle::Win32WindowHandle::empty()) +//! # } +//! # } +//! # let window = T; +//! let webview = WebViewBuilder::new(&window) +//! .with_url("https://tauri.app") +//! .unwrap() +//! .build() +//! .unwrap(); +//! ``` +//! +//! If you also want to support Wayland too, then we recommend you use [`WebViewBuilder::new_gtk`] on Linux. +//! +//! ```no_run,ignore +//! use wry::WebViewBuilder; +//! +//! #[cfg(any( +//! target_os = "windows", +//! target_os = "macos", +//! target_os = "ios", +//! target_os = "android" +//! ))] +//! let builder = WebViewBuilder::new(&window); +//! #[cfg(not(any( +//! target_os = "windows", +//! target_os = "macos", +//! target_os = "ios", +//! target_os = "android" +//! )))] +//! let builder = { +//! use tao::platform::unix::WindowExtUnix; +//! WebViewBuilder::new_gtk(&window.gtk_window()) +//! }; +//! +//! let webview = builder +//! .with_url("https://tauri.app") +//! .unwrap() +//! .build() +//! .unwrap(); +//! ``` +//! +//! ## Child webviews +//! +//! You can use [`WebView::new_as_child`] to create the webview as a child inside another window. This is supported on +//! macOS, Windows and Linux (X11 Only). //! //! ```no_run -//! fn main() -> wry::Result<()> { -//! use wry::{ -//! application::{ -//! event::{Event, StartCause, WindowEvent}, -//! event_loop::{ControlFlow, EventLoop}, -//! window::WindowBuilder, -//! }, -//! webview::WebViewBuilder, -//! }; -//! -//! let event_loop = EventLoop::new(); -//! let window = WindowBuilder::new() -//! .with_title("Hello World") -//! .build(&event_loop)?; -//! let _webview = WebViewBuilder::new(window)? -//! .with_url("https://tauri.studio")? -//! .build()?; -//! -//! event_loop.run(move |event, _, control_flow| { -//! *control_flow = ControlFlow::Wait; -//! -//! match event { -//! Event::NewEvents(StartCause::Init) => println!("Wry has started!"), -//! Event::WindowEvent { -//! event: WindowEvent::CloseRequested, -//! .. -//! } => *control_flow = ControlFlow::Exit, -//! _ => (), +//! use wry::WebViewBuilder; +//! +//! # struct T; +//! # unsafe impl raw_window_handle::HasRawWindowHandle for T { +//! # fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { +//! # raw_window_handle::RawWindowHandle::Win32(raw_window_handle::Win32WindowHandle::empty()) +//! # } +//! # } +//! # let window = T; +//! let webview = WebViewBuilder::new_as_child(&window) +//! .with_url("https://tauri.app") +//! .unwrap() +//! .build() +//! .unwrap(); +//! ``` +//! +//! ## Platform Considerations +//! +//! Note that on Linux, we use webkit2gtk webviews so if the windowing library doesn't support gtk (as in [`winit`]) +//! you'll need to call [`gtk::init`] before creating the webview and then call [`gtk::main_iteration_do`] alongside +//! your windowing library event loop. +//! +//! ```no_run,ignore +//! use winit::{event_loop::EventLoop, window::Window}; +//! use wry::WebView; +//! +//! fn main() { +//! let event_loop = EventLoop::new().unwrap(); +//! gtk::init().unwrap(); // <----- IMPORTANT +//! let window = Window::new(&event_loop).unwrap(); +//! let webview = WebView::new(&window); +//! event_loop.run(|_e, _evl|{ +//! // process winit events +//! +//! // then advance gtk event loop <----- IMPORTANT +//! while gtk::events_pending() { +//! gtk::main_iteration_do(false); //! } -//! }); +//! }).unwrap(); +//! } +//! ``` +//! +//! ## Android +//! +//! In order for `wry` to be able to create webviews on Android, there is a few requirements that your application needs to uphold: +//! 1. You need to set a few environment variables that will be used to generate the necessary kotlin +//! files that you need to include in your Android application for wry to function properly. +//! - `WRY_ANDROID_PACKAGE`: which is the reversed domain name of your android project and the app name in snake_case, for example, `com.wry.example.wry_app` +//! - `WRY_ANDROID_LIBRARY`: for example, if your cargo project has a lib name `wry_app`, it will generate `libwry_app.so` so you se this env var to `wry_app` +//! - `WRY_ANDROID_KOTLIN_FILES_OUT_DIR`: for example, `path/to/app/src/main/kotlin/com/wry/example` +//! 2. Your main Android Activity needs to inherit `AppCompatActivity`, preferably it should use the generated `WryActivity` or inherit it. +//! 3. Your Rust app needs to call `wry::android_setup` function to setup the necessary logic to be able to create webviews later on. +//! 4. Your Rust app needs to call `wry::android_binding!` macro to setup the JNI functions that will be called by `WryActivity` and various other places. +//! +//! It is recommended to use [`tao`](https://docs.rs/tao/latest/tao/) crate as it provides maximum compatibility with `wry` +//! +//! ``` +//! #[cfg(target_os = "android")] +//! { +//! tao::android_binding!( +//! com_example, +//! wry_app, +//! WryActivity, +//! wry::android_setup, // pass the wry::android_setup function to tao which will invoke when the event loop is created +//! _start_app +//! ); +//! wry::android_binding!(com_example, ttt); //! } //! ``` //! +//! If this feels overwhelming, you can just use the preconfigured template from [`cargo-mobile2`](https://github.com/tauri-apps/cargo-mobile2). +//! +//! For more inforamtion, checkout [MOBILE.md](https://github.com/tauri-apps/wry/blob/dev/MOBILE.md). +//! //! ## Feature flags //! -//! Wry uses a set of feature flags to toggle several advanced features. `file-drop`, `protocol`, -//! are enabled by default. -//! -//! - `tao`: Default windowing crate used by wry. It is re-exported as `application` module. -//! - `winit`: Replace [tao] with [winit] crate. It only supports Windows and macOS. -//! - `file-drop`: Enables [`with_file_drop_handler`] to control the behaviour when there are files -//! interacting with the window. Enabled by default. -//! - `protocol`: Enables [`with_custom_protocol`] to define custom URL scheme for handling tasks like -//! loading assets. Enabled by default. -//! This feature requires either `libayatana-appindicator` or `libappindicator` package installed. -//! You can still create those types if you disable it. They just don't create the actual objects. +//! Wry uses a set of feature flags to toggle several advanced features. +//! +//! - `protocol` (default): Enables [`WebViewBuilder::with_custom_protocol`] to define custom URL scheme for handling tasks like +//! loading assets. +//! - `file-drop` (default): Enables [`WebViewBuilder::with_file_drop_handler`] to control the behaviour when there are files +//! interacting with the window. //! - `devtools`: Enables devtools on release builds. Devtools are always enabled in debug builds. //! On **macOS**, enabling devtools, requires calling private apis so you should not enable this flag in release //! build if your app needs to publish to App Store. @@ -64,109 +157,1420 @@ //! - `fullscreen`: Fullscreen video and other media on **macOS** requires calling private functions. //! Avoid this in release build if your app needs to publish to App Store. //! libraries and prevent from building documentation on doc.rs fails. -//! - `linux-headers`: Enables headers support of custom protocol request on Linux. Requires -//! webkit2gtk v2.36 or above. +//! - `linux-body`: Enables body support of custom protocol request on Linux. Requires +//! webkit2gtk v2.40 or above. //! -//! [tao]: https://crates.io/crates/tao -//! [`EventLoop`]: crate::application::event_loop::EventLoop -//! [`Window`]: crate::application::window::Window -//! [`WebView`]: crate::webview::WebView -//! [`with_file_drop_handler`]: crate::webview::WebView::with_file_drop_handler -//! [`with_custom_protocol`]: crate::webview::WebView::with_custom_protocol +//! [`tao`]: https://docs.rs/tao +//! [`winit`]: https://docs.rs/winit #![allow(clippy::new_without_default)] -#![allow(clippy::wrong_self_convention)] +#![allow(clippy::default_constructed_unit_structs)] #![allow(clippy::type_complexity)] -#![allow(clippy::unit_cmp)] -#![allow(clippy::upper_case_acronyms)] #![cfg_attr(docsrs, feature(doc_cfg))] -#[macro_use] -extern crate serde; -#[macro_use] -extern crate thiserror; #[cfg(any(target_os = "macos", target_os = "ios"))] #[macro_use] extern crate objc; -use std::sync::mpsc::{RecvError, SendError}; +mod error; +mod proxy; +mod web_context; + +#[cfg(target_os = "android")] +pub(crate) mod android; +#[cfg(target_os = "android")] +pub use crate::android::android_setup; +#[cfg(target_os = "android")] +pub mod prelude { + pub use crate::android::{binding::*, dispatch, find_class, Context}; + pub use tao_macros::{android_fn, generate_package_name}; +} +#[cfg(target_os = "android")] +pub use android::JniHandle; +#[cfg(target_os = "android")] +use android::*; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +pub(crate) mod webkitgtk; +use raw_window_handle::HasRawWindowHandle; +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use webkitgtk::*; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub(crate) mod wkwebview; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use wkwebview::*; + +#[cfg(target_os = "windows")] +pub(crate) mod webview2; +#[cfg(target_os = "windows")] +use self::webview2::*; +#[cfg(target_os = "windows")] +use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller; + +use std::{borrow::Cow, path::PathBuf, rc::Rc}; -use crate::application::window::BadIcon; -pub use serde_json::Value; -use url::ParseError; +use http::{Request, Response}; -pub mod application; +pub use error::*; pub use http; -pub mod webview; +pub use proxy::{ProxyConfig, ProxyEndpoint}; +pub use url::Url; +pub use web_context::WebContext; -/// Convenient type alias of Result type for wry. -pub type Result = std::result::Result; +/// Resolves a custom protocol [`Request`] asynchronously. +/// +/// See [`WebViewBuilder::with_asynchronous_custom_protocol`] for more information. +pub struct RequestAsyncResponder { + pub(crate) responder: Box>)>, +} -/// Errors returned by wry. -#[non_exhaustive] -#[derive(Error, Debug)] -pub enum Error { +// SAFETY: even though the webview bindings do not indicate the responder is Send, +// it actually is and we need it in order to let the user do the protocol computation +// on a separate thread or async task. +unsafe impl Send for RequestAsyncResponder {} + +impl RequestAsyncResponder { + /// Resolves the request with the given response. + pub fn respond>>(self, response: Response) { + let (parts, body) = response.into_parts(); + (self.responder)(Response::from_parts(parts, body.into())) + } +} + +pub struct WebViewAttributes { + /// Whether the WebView should have a custom user-agent. + pub user_agent: Option, + + /// Whether the WebView window should be visible. + pub visible: bool, + + /// Whether the WebView should be transparent. + /// + /// ## Platform-specific: + /// + /// **Windows 7**: Not supported. + pub transparent: bool, + + /// Specify the webview background color. This will be ignored if `transparent` is set to `true`. + /// + /// The color uses the RGBA format. + /// + /// ## Platform-specific: + /// + /// - **macOS / iOS**: Not implemented. + /// - **Windows**: + /// - On Windows 7, transparency is not supported and the alpha value will be ignored. + /// - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + pub background_color: Option, + + /// Whether load the provided URL to [`WebView`]. + pub url: Option, + + /// Headers used when loading the requested `url`. + pub headers: Option, + + /// Whether page zooming by hotkeys is enabled + /// + /// ## Platform-specific + /// + /// **macOS / Linux / Android / iOS**: Unsupported + pub zoom_hotkeys_enabled: bool, + + /// Whether load the provided html string to [`WebView`]. + /// This will be ignored if the `url` is provided. + /// + /// # Warning + /// + /// The Page loaded from html string will have `null` origin. + /// + /// ## PLatform-specific: + /// + /// - **Windows:** the string can not be larger than 2 MB (2 * 1024 * 1024 bytes) in total size + pub html: Option, + + /// Initialize javascript code when loading new pages. When webview load a new page, this + /// initialization code will be executed. It is guaranteed that code is executed before + /// `window.onload`. + /// + /// ## Platform-specific + /// + /// - **Android:** The Android WebView does not provide an API for initialization scripts, + /// so we prepend them to each HTML head. They are only implemented on custom protocol URLs. + pub initialization_scripts: Vec, + + /// A list of custom loading protocols with pairs of scheme uri string and a handling + /// closure. + /// + /// The closure takes a [Request] and returns a [Response]. + /// + /// # Warning + /// + /// Pages loaded from custom protocol will have different Origin on different platforms. And + /// servers which enforce CORS will need to add exact same Origin header in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://` (so it will be `wry://path/to/page/`). + /// - Windows and Android: `http://.` by default (so it will be `http://wry.path/to/page). To use `https` instead of `http`, use [`WebViewBuilderExtWindows::with_https_scheme`] and [`WebViewBuilderExtAndroid::with_https_scheme`]. + /// + /// # Reading assets on mobile + /// + /// - Android: Android has `assets` and `resource` path finder to + /// locate your files in those directories. For more information, see [Loading in-app content](https://developer.android.com/guide/webapps/load-local-content) page. + /// - iOS: To get the path of your assets, you can call [`CFBundle::resources_path`](https://docs.rs/core-foundation/latest/core_foundation/bundle/struct.CFBundle.html#method.resources_path). So url like `wry://assets/index.html` could get the html file in assets directory. + pub custom_protocols: Vec<(String, Box>, RequestAsyncResponder)>)>, + + /// The IPC handler to receive the message from Javascript on webview + /// using `window.ipc.postMessage("insert_message_here")` to host Rust code. + pub ipc_handler: Option>, + + /// A handler closure to process incoming [`FileDropEvent`] of the webview. + /// + /// # Blocking OS Default Behavior + /// Return `true` in the callback to block the OS' default behavior of handling a file drop. + /// + /// Note, that if you do block this behavior, it won't be possible to drop files on `` forms. + /// Also note, that it's not possible to manually set the value of a `` via JavaScript for security reasons. + #[cfg(feature = "file-drop")] + pub file_drop_handler: Option bool>>, + #[cfg(not(feature = "file-drop"))] + file_drop_handler: Option bool>>, + + /// A navigation handler to decide if incoming url is allowed to navigate. + /// + /// The closure take a `String` parameter as url and returns a `bool` to determine whether the navigation should happen. + /// `true` allows to navigate and `false` does not. + pub navigation_handler: Option bool>>, + + /// A download started handler to manage incoming downloads. + /// + /// The closure takes two parameters, the first is a `String` representing the url being downloaded from and and the + /// second is a mutable `PathBuf` reference that (possibly) represents where the file will be downloaded to. The latter + /// parameter can be used to set the download location by assigning a new path to it, the assigned path _must_ be + /// absolute. The closure returns a `bool` to allow or deny the download. + pub download_started_handler: Option bool>>, + + /// A download completion handler to manage downloads that have finished. + /// + /// The closure is fired when the download completes, whether it was successful or not. + /// The closure takes a `String` representing the URL of the original download request, an `Option` + /// potentially representing the filesystem path the file was downloaded to, and a `bool` indicating if the download + /// succeeded. A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download + /// did not succeed, and may instead indicate some other failure, always check the third parameter if you need to + /// know if the download succeeded. + /// + /// ## Platform-specific: + /// + /// - **macOS**: The second parameter indicating the path the file was saved to, is always empty, + /// due to API limitations. + pub download_completed_handler: Option, bool) + 'static>>, + + /// A new window handler to decide if incoming url is allowed to open in a new window. + /// + /// The closure take a `String` parameter as url and return `bool` to determine whether the window should open. + /// `true` allows to open and `false` does not. + pub new_window_req_handler: Option bool>>, + + /// Enables clipboard access for the page rendered on **Linux** and **Windows**. + /// + /// macOS doesn't provide such method and is always enabled by default. But your app will still need to add menu + /// item accelerators to use the clipboard shortcuts. + pub clipboard: bool, + + /// Enable web inspector which is usually called browser devtools. + /// + /// Note this only enables devtools to the webview. To open it, you can call + /// [`WebView::open_devtools`], or right click the page and open it from the context menu. + /// + /// ## Platform-specific + /// + /// - macOS: This will call private functions on **macOS**. It is enabled in **debug** builds, + /// but requires `devtools` feature flag to actually enable it in **release** builds. + /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + pub devtools: bool, + + /// Whether clicking an inactive window also clicks through to the webview. Default is `false`. + /// + /// ## Platform-specific + /// + /// This configuration only impacts macOS. + pub accept_first_mouse: bool, + + /// Indicates whether horizontal swipe gestures trigger backward and forward page navigation. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported. + pub back_forward_navigation_gestures: bool, + + /// Set a handler closure to process the change of the webview's document title. + pub document_title_changed_handler: Option>, + + /// Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is + /// enabled. + /// + /// ## Platform-specific: + /// + /// - **Android:** Unsupported yet. + pub incognito: bool, + + /// Whether all media can be played without user interaction. + pub autoplay: bool, + + /// Set a handler closure to process page load events. + pub on_page_load_handler: Option>, + + /// Set a proxy configuration for the webview. Supports HTTP CONNECT and SOCKSv5 proxies + /// + /// - **macOS**: Requires macOS 14.0+ and the `mac-proxy` feature flag to be enabled. + /// - **Android / iOS:** Not supported. + pub proxy_config: Option, + + /// Whether the webview should be focused when created. + /// + /// ## Platform-specific: + /// + /// - **macOS / Android / iOS:** Unsupported. + pub focused: bool, + + /// The webview postion. + /// This is effective if the webview was created by [`WebView::new_as_child`]. + /// If it's `None`, the position will be (0, 0). + pub position: Option<(i32, i32)>, + + /// The webview size. + /// This is effective if the webview was created by [`WebView::new_as_child`]. + /// If it's `None`, the size will be (0, 0). + pub size: Option<(u32, u32)>, +} + +impl Default for WebViewAttributes { + fn default() -> Self { + Self { + user_agent: None, + visible: true, + transparent: false, + background_color: None, + url: None, + headers: None, + html: None, + initialization_scripts: vec![], + custom_protocols: vec![], + ipc_handler: None, + file_drop_handler: None, + navigation_handler: None, + download_started_handler: None, + download_completed_handler: None, + new_window_req_handler: None, + clipboard: false, + #[cfg(debug_assertions)] + devtools: true, + #[cfg(not(debug_assertions))] + devtools: false, + zoom_hotkeys_enabled: false, + accept_first_mouse: false, + back_forward_navigation_gestures: false, + document_title_changed_handler: None, + incognito: false, + autoplay: true, + on_page_load_handler: None, + proxy_config: None, + focused: true, + position: None, + size: None, + } + } +} + +/// Builder type of [`WebView`]. +/// +/// [`WebViewBuilder`] / [`WebView`] are the basic building blocks to construct WebView contents and +/// scripts for those who prefer to control fine grained window creation and event handling. +/// [`WebViewBuilder`] provides ability to setup initialization before web engine starts. +pub struct WebViewBuilder<'a> { + pub attrs: WebViewAttributes, + as_child: bool, + window: Option<&'a dyn HasRawWindowHandle>, + platform_specific: PlatformSpecificWebViewAttributes, + web_context: Option<&'a mut WebContext>, #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", - target_os = "openbsd" + target_os = "openbsd", ))] - #[error(transparent)] - GlibError(#[from] gtk::glib::Error), + gtk_widget: Option<&'a gtk::Container>, +} + +impl<'a> WebViewBuilder<'a> { + /// Create a [`WebViewBuilder`] from a type that implements [`HasRawWindowHandle`]. + /// + /// # Platform-specific: + /// + /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use [`WebViewBuilder::new_gtk`]. + /// + /// Although this methods only needs an X11 window handle, we use webkit2gtk, so you still need to initialize gtk + /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. + /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. + /// - **Windows**: The webview will auto-resize when the passed handle is resized. + /// - **Linux (X11)**: Unlike macOS and Windows, the webview will not auto-resize and you'll need to call [`WebView::set_size`] manually. + /// + /// # Panics: + /// + /// - Panics if the provided handle was not supported or invalid. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + pub fn new(window: &'a impl HasRawWindowHandle) -> Self { + Self { + attrs: WebViewAttributes::default(), + window: Some(window), + as_child: false, + #[allow(clippy::default_constructed_unit_structs)] + platform_specific: PlatformSpecificWebViewAttributes::default(), + web_context: None, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + gtk_widget: None, + } + } + + /// Create [`WebViewBuilder`] as a child window inside the provided [`HasRawWindowHandle`]. + /// + /// ## Platform-specific + /// + /// - **Windows**: This will create the webview as a child window of the `parent` window. + /// - **macOS**: This will create the webview as a `NSView` subview of the `parent` window's + /// content view. + /// - **Linux**: This will create the webview as a child window of the `parent` window. Only X11 + /// is supported. This method won't work on Wayland. + /// + /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk + /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. + /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. + /// - **Android/iOS:** Unsupported. + /// + /// # Panics: + /// + /// - Panics if the provided handle was not support or invalid. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + pub fn new_as_child(parent: &'a impl HasRawWindowHandle) -> Self { + Self { + attrs: WebViewAttributes::default(), + window: Some(parent), + as_child: true, + #[allow(clippy::default_constructed_unit_structs)] + platform_specific: PlatformSpecificWebViewAttributes::default(), + web_context: None, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + gtk_widget: None, + } + } + #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", - target_os = "openbsd" + target_os = "openbsd", ))] - #[error(transparent)] - GlibBoolError(#[from] gtk::glib::BoolError), + /// Create the webview from a GTK container widget, such as GTK window. + /// + /// # Panics: + /// + /// - Panics if [`gtk::init`] was not called in this thread. + pub fn new_gtk(widget: &'a W) -> Self + where + W: gtk::prelude::IsA, + { + use gdkx11::glib::Cast; + + Self { + attrs: WebViewAttributes::default(), + window: None, + as_child: false, + #[allow(clippy::default_constructed_unit_structs)] + platform_specific: PlatformSpecificWebViewAttributes::default(), + web_context: None, + gtk_widget: Some(widget.dynamic_cast_ref().unwrap()), + } + } + + /// Indicates whether horizontal swipe gestures trigger backward and forward page navigation. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported. + pub fn with_back_forward_navigation_gestures(mut self, gesture: bool) -> Self { + self.attrs.back_forward_navigation_gestures = gesture; + self + } + + /// Sets whether the WebView should be transparent. + /// + /// ## Platform-specific: + /// + /// **Windows 7**: Not supported. + pub fn with_transparent(mut self, transparent: bool) -> Self { + self.attrs.transparent = transparent; + self + } + + /// Specify the webview background color. This will be ignored if `transparent` is set to `true`. + /// + /// The color uses the RGBA format. + /// + /// ## Platfrom-specific: + /// + /// - **macOS / iOS**: Not implemented. + /// - **Windows**: + /// - on Windows 7, transparency is not supported and the alpha value will be ignored. + /// - on Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + pub fn with_background_color(mut self, background_color: RGBA) -> Self { + self.attrs.background_color = Some(background_color); + self + } + + /// Sets whether the WebView should be visible or not. + pub fn with_visible(mut self, visible: bool) -> Self { + self.attrs.visible = visible; + self + } + + /// Sets whether all media can be played without user interaction. + pub fn with_autoplay(mut self, autoplay: bool) -> Self { + self.attrs.autoplay = autoplay; + self + } + + /// Initialize javascript code when loading new pages. When webview load a new page, this + /// initialization code will be executed. It is guaranteed that code is executed before + /// `window.onload`. + /// + /// ## Platform-specific + /// + /// - **Android:** The Android WebView does not provide an API for initialization scripts, + /// so we prepend them to each HTML head. They are only implemented on custom protocol URLs. + pub fn with_initialization_script(mut self, js: &str) -> Self { + if !js.is_empty() { + self.attrs.initialization_scripts.push(js.to_string()); + } + self + } + + /// Register custom loading protocols with pairs of scheme uri string and a handling + /// closure. + /// + /// The closure takes a [Request] and returns a [Response] + /// + /// # Warning + /// + /// Pages loaded from custom protocol will have different Origin on different platforms. And + /// servers which enforce CORS will need to add exact same Origin header in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://` (so it will be `wry://path/to/page). + /// - Windows and Android: `http://.` by default (so it will be `http://wry.path/to/page`). To use `https` instead of `http`, use [`WebViewBuilderExtWindows::with_https_scheme`] and [`WebViewBuilderExtAndroid::with_https_scheme`]. + /// + /// # Reading assets on mobile + /// + /// - Android: For loading content from the `assets` folder (which is copied to the Andorid apk) please + /// use the function [`with_asset_loader`] from [`WebViewBuilderExtAndroid`] instead. + /// This function on Android can only be used to serve assets you can embed in the binary or are + /// elsewhere in Android (provided the app has appropriate access), but not from the `assets` + /// folder which lives within the apk. For the cases where this can be used, it works the same as in macOS and Linux. + /// - iOS: To get the path of your assets, you can call [`CFBundle::resources_path`](https://docs.rs/core-foundation/latest/core_foundation/bundle/struct.CFBundle.html#method.resources_path). So url like `wry://assets/index.html` could get the html file in assets directory. + #[cfg(feature = "protocol")] + pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self + where + F: Fn(Request>) -> Response> + 'static, + { + self.attrs.custom_protocols.push(( + name, + Box::new(move |request, responder| { + let http_response = handler(request); + responder.respond(http_response); + }), + )); + self + } + + /// Same as [`Self::with_custom_protocol`] but with an asynchronous responder. + /// + /// # Examples + /// + /// ```no_run + /// use wry::WebViewBuilder; + /// + /// # struct T; + /// # unsafe impl raw_window_handle::HasRawWindowHandle for T { + /// # fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { + /// # raw_window_handle::RawWindowHandle::Win32(raw_window_handle::Win32WindowHandle::empty()) + /// # } + /// # } + /// # let window = T; + /// WebViewBuilder::new(&window) + /// .with_asynchronous_custom_protocol("wry".into(), |request, responder| { + /// // here you can use a tokio task, thread pool or anything + /// // to do heavy computation to resolve your request + /// // e.g. downloading files, opening the camera... + /// std::thread::spawn(move || { + /// std::thread::sleep(std::time::Duration::from_secs(2)); + /// responder.respond(http::Response::builder().body(Vec::new()).unwrap()); + /// }); + /// }); + /// ``` + #[cfg(feature = "protocol")] + pub fn with_asynchronous_custom_protocol(mut self, name: String, handler: F) -> Self + where + F: Fn(Request>, RequestAsyncResponder) + 'static, + { + self.attrs.custom_protocols.push((name, Box::new(handler))); + self + } + + /// Set the IPC handler to receive the message from Javascript on webview + /// using `window.ipc.postMessage("insert_message_here")` to host Rust code. + pub fn with_ipc_handler(mut self, handler: F) -> Self + where + F: Fn(String) + 'static, + { + self.attrs.ipc_handler = Some(Box::new(handler)); + self + } + + /// Set a handler closure to process incoming [`FileDropEvent`] of the webview. + /// + /// # Blocking OS Default Behavior + /// Return `true` in the callback to block the OS' default behavior of handling a file drop. + /// + /// Note, that if you do block this behavior, it won't be possible to drop files on `` forms. + /// Also note, that it's not possible to manually set the value of a `` via JavaScript for security reasons. + #[cfg(feature = "file-drop")] + pub fn with_file_drop_handler(mut self, handler: F) -> Self + where + F: Fn(FileDropEvent) -> bool + 'static, + { + self.attrs.file_drop_handler = Some(Box::new(handler)); + self + } + + /// Load the provided URL with given headers when the builder calling [`WebViewBuilder::build`] to create the [`WebView`]. + /// The provided URL must be valid. + pub fn with_url_and_headers(mut self, url: &str, headers: http::HeaderMap) -> Result { + self.attrs.url = Some(url.parse()?); + self.attrs.headers = Some(headers); + Ok(self) + } + + /// Load the provided URL when the builder calling [`WebViewBuilder::build`] to create the [`WebView`]. + /// The provided URL must be valid. + pub fn with_url(mut self, url: &str) -> Result { + self.attrs.url = Some(Url::parse(url)?); + self.attrs.headers = None; + Ok(self) + } + + /// Load the provided HTML string when the builder calling [`WebViewBuilder::build`] to create the [`WebView`]. + /// This will be ignored if `url` is provided. + /// + /// # Warning + /// + /// The Page loaded from html string will have `null` origin. + /// + /// ## PLatform-specific: + /// + /// - **Windows:** the string can not be larger than 2 MB (2 * 1024 * 1024 bytes) in total size + pub fn with_html(mut self, html: impl Into) -> Result { + self.attrs.html = Some(html.into()); + Ok(self) + } + + /// Set the web context that can be shared with multiple [`WebView`]s. + pub fn with_web_context(mut self, web_context: &'a mut WebContext) -> Self { + self.web_context = Some(web_context); + self + } + + /// Set a custom [user-agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) for the WebView. + pub fn with_user_agent(mut self, user_agent: &str) -> Self { + self.attrs.user_agent = Some(user_agent.to_string()); + self + } + + /// Enable or disable web inspector which is usually called devtools. + /// + /// Note this only enables devtools to the webview. To open it, you can call + /// [`WebView::open_devtools`], or right click the page and open it from the context menu. + /// + /// ## Platform-specific + /// + /// - macOS: This will call private functions on **macOS**. It is enabled in **debug** builds, + /// but requires `devtools` feature flag to actually enable it in **release** builds. + /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + pub fn with_devtools(mut self, devtools: bool) -> Self { + self.attrs.devtools = devtools; + self + } + + /// Whether page zooming by hotkeys or gestures is enabled + /// + /// ## Platform-specific + /// + /// **macOS / Linux / Android / iOS**: Unsupported + pub fn with_hotkeys_zoom(mut self, zoom: bool) -> Self { + self.attrs.zoom_hotkeys_enabled = zoom; + self + } + + /// Set a navigation handler to decide if incoming url is allowed to navigate. + /// + /// The closure take a `String` parameter as url and returns a `bool` to determine whether the navigation should happen. + /// `true` allows to navigate and `false` does not. + pub fn with_navigation_handler(mut self, callback: impl Fn(String) -> bool + 'static) -> Self { + self.attrs.navigation_handler = Some(Box::new(callback)); + self + } + + /// Set a download started handler to manage incoming downloads. + /// + //// The closure takes two parameters, the first is a `String` representing the url being downloaded from and and the + /// second is a mutable `PathBuf` reference that (possibly) represents where the file will be downloaded to. The latter + /// parameter can be used to set the download location by assigning a new path to it, the assigned path _must_ be + /// absolute. The closure returns a `bool` to allow or deny the download. + pub fn with_download_started_handler( + mut self, + started_handler: impl FnMut(String, &mut PathBuf) -> bool + 'static, + ) -> Self { + self.attrs.download_started_handler = Some(Box::new(started_handler)); + self + } + + /// Sets a download completion handler to manage downloads that have finished. + /// + /// The closure is fired when the download completes, whether it was successful or not. + /// The closure takes a `String` representing the URL of the original download request, an `Option` + /// potentially representing the filesystem path the file was downloaded to, and a `bool` indicating if the download + /// succeeded. A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download + /// did not succeed, and may instead indicate some other failure, always check the third parameter if you need to + /// know if the download succeeded. + /// + /// ## Platform-specific: + /// + /// - **macOS**: The second parameter indicating the path the file was saved to, is always empty, + /// due to API limitations. + pub fn with_download_completed_handler( + mut self, + download_completed_handler: impl Fn(String, Option, bool) + 'static, + ) -> Self { + self.attrs.download_completed_handler = Some(Rc::new(download_completed_handler)); + self + } + + /// Enables clipboard access for the page rendered on **Linux** and **Windows**. + /// + /// macOS doesn't provide such method and is always enabled by default. But your app will still need to add menu + /// item accelerators to use the clipboard shortcuts. + pub fn with_clipboard(mut self, clipboard: bool) -> Self { + self.attrs.clipboard = clipboard; + self + } + + /// Set a new window request handler to decide if incoming url is allowed to be opened. + /// + /// The closure take a `String` parameter as url and return `bool` to determine whether the window should open. + /// `true` allows to open and `false` does not. + pub fn with_new_window_req_handler( + mut self, + callback: impl Fn(String) -> bool + 'static, + ) -> Self { + self.attrs.new_window_req_handler = Some(Box::new(callback)); + self + } + + /// Sets whether clicking an inactive window also clicks through to the webview. Default is `false`. + /// + /// ## Platform-specific + /// + /// This configuration only impacts macOS. + pub fn with_accept_first_mouse(mut self, accept_first_mouse: bool) -> Self { + self.attrs.accept_first_mouse = accept_first_mouse; + self + } + + /// Set a handler closure to process the change of the webview's document title. + pub fn with_document_title_changed_handler( + mut self, + callback: impl Fn(String) + 'static, + ) -> Self { + self.attrs.document_title_changed_handler = Some(Box::new(callback)); + self + } + + /// Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is + /// enabled. + /// + /// ## Platform-specific: + /// + /// - **Android:** Unsupported yet. + pub fn with_incognito(mut self, incognito: bool) -> Self { + self.attrs.incognito = incognito; + self + } + + /// Set a handler to process page loading events. + pub fn with_on_page_load_handler( + mut self, + handler: impl Fn(PageLoadEvent, String) + 'static, + ) -> Self { + self.attrs.on_page_load_handler = Some(Box::new(handler)); + self + } + + /// Set a proxy configuration for the webview. + /// + /// - **macOS**: Requires macOS 14.0+ and the `mac-proxy` feature flag to be enabled. Supports HTTP CONNECT and SOCKSv5 proxies. + /// - **Windows / Linux**: Supports HTTP CONNECT and SOCKSv5 proxies. + /// - **Android / iOS:** Not supported. + pub fn with_proxy_config(mut self, configuration: ProxyConfig) -> Self { + self.attrs.proxy_config = Some(configuration); + self + } + + /// Set whether the webview should be focused when created. + /// + /// ## Platform-specific: + /// + /// - **macOS / Android / iOS:** Unsupported. + pub fn with_focused(mut self, focused: bool) -> Self { + self.attrs.focused = focused; + self + } + + /// Set the webview position relative to its parent if it was created as a child. + pub fn with_position(mut self, position: (i32, i32)) -> Self { + self.attrs.position = Some(position); + self + } + + /// Set the webview size if it was created as a child. + pub fn with_size(mut self, size: (u32, u32)) -> Self { + self.attrs.size = Some(size); + self + } + + /// Consume the builder and create the [`WebView`]. + /// + /// # Panics: + /// + /// - Panics if the provided handle was not support or invalid. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + pub fn build(self) -> Result { + let webview = if let Some(window) = &self.window { + if self.as_child { + InnerWebView::new_as_child(window, self.attrs, self.platform_specific, self.web_context)? + } else { + InnerWebView::new(window, self.attrs, self.platform_specific, self.web_context)? + } + } else { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + if let Some(widget) = self.gtk_widget { + InnerWebView::new_gtk(widget, self.attrs, self.platform_specific, self.web_context)? + } else { + unreachable!() + } + + #[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + )))] + unreachable!() + }; + + Ok(WebView { webview }) + } +} + +#[cfg(windows)] +#[derive(Clone)] +pub(crate) struct PlatformSpecificWebViewAttributes { + additional_browser_args: Option, + browser_accelerator_keys: bool, + theme: Option, + https_scheme: bool, +} + +#[cfg(windows)] +impl Default for PlatformSpecificWebViewAttributes { + fn default() -> Self { + Self { + additional_browser_args: None, + browser_accelerator_keys: true, // This is WebView2's default behavior + theme: None, + https_scheme: false, // To match macOS & Linux behavior in the context of mixed content. + } + } +} + +#[cfg(windows)] +pub trait WebViewBuilderExtWindows { + /// Pass additional args to Webview2 upon creating the webview. + /// + /// ## Warning + /// + /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` + /// `--autoplay-policy=no-user-gesture-required` if autoplay is enabled + /// and `--proxy-server=://:` if a proxy is set. + /// so if you use this method, you have to add these arguments yourself if you want to keep the same behavior. + fn with_additional_browser_args>(self, additional_args: S) -> Self; + + /// Determines whether browser-specific accelerator keys are enabled. When this setting is set to + /// `false`, it disables all accelerator keys that access features specific to a web browser. + /// The default value is `true`. See the following link to know more details. + /// + /// https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings#arebrowseracceleratorkeysenabled + fn with_browser_accelerator_keys(self, enabled: bool) -> Self; + + /// Specifies the theme of webview2. This affects things like `prefers-color-scheme`. + /// + /// Defaults to [`Theme::Auto`] which will follow the OS defaults. + fn with_theme(self, theme: Theme) -> Self; + + /// Determines whether the custom protocols should use `https://.path/to/page` instead of the default `http://.path/to/page`. + /// + /// Using a `http` scheme will allow mixed content when trying to fetch `http` endpoints + /// and is therefore less secure but will match the behavior of the `://path/to/page` protocols used on macOS and Linux. + /// + /// The default value is `false`. + fn with_https_scheme(self, enabled: bool) -> Self; +} + +#[cfg(windows)] +impl WebViewBuilderExtWindows for WebViewBuilder<'_> { + fn with_additional_browser_args>(mut self, additional_args: S) -> Self { + self.platform_specific.additional_browser_args = Some(additional_args.into()); + self + } + + fn with_browser_accelerator_keys(mut self, enabled: bool) -> Self { + self.platform_specific.browser_accelerator_keys = enabled; + self + } + + fn with_theme(mut self, theme: Theme) -> Self { + self.platform_specific.theme = Some(theme); + self + } + + fn with_https_scheme(mut self, enabled: bool) -> Self { + self.platform_specific.https_scheme = enabled; + self + } +} + +#[cfg(target_os = "android")] +#[derive(Default)] +pub(crate) struct PlatformSpecificWebViewAttributes { + on_webview_created: + Option std::result::Result<(), jni::errors::Error> + Send>>, + with_asset_loader: bool, + asset_loader_domain: Option, + https_scheme: bool, +} + +#[cfg(target_os = "android")] +pub trait WebViewBuilderExtAndroid { + fn on_webview_created< + F: Fn(prelude::Context<'_, '_>) -> std::result::Result<(), jni::errors::Error> + Send + 'static, + >( + self, + f: F, + ) -> Self; + + /// Use [WebviewAssetLoader](https://developer.android.com/reference/kotlin/androidx/webkit/WebViewAssetLoader) + /// to load assets from Android's `asset` folder when using `with_url` as `://assets/` (e.g.: + /// `wry://assets/index.html`). Note that this registers a custom protocol with the provided + /// String, similar to [`with_custom_protocol`], but also sets the WebViewAssetLoader with the + /// necessary domain (which is fixed as `.assets`). This cannot be used in conjunction + /// to `with_custom_protocol` for Android, as it changes the way in which requests are handled. + #[cfg(feature = "protocol")] + fn with_asset_loader(self, protocol: String) -> Self; + + /// Determines whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost`. + /// + /// Using a `http` scheme will allow mixed content when trying to fetch `http` endpoints + /// and is therefore less secure but will match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// The default value is `false`. + fn with_https_scheme(self, enabled: bool) -> Self; +} + +#[cfg(target_os = "android")] +impl WebViewBuilderExtAndroid for WebViewBuilder<'_> { + fn on_webview_created< + F: Fn(prelude::Context<'_, '_>) -> std::result::Result<(), jni::errors::Error> + Send + 'static, + >( + mut self, + f: F, + ) -> Self { + self.platform_specific.on_webview_created = Some(Box::new(f)); + self + } + + #[cfg(feature = "protocol")] + fn with_asset_loader(mut self, protocol: String) -> Self { + // register custom protocol with empty Response return, + // this is necessary due to the need of fixing a domain + // in WebViewAssetLoader. + self.attrs.custom_protocols.push(( + protocol.clone(), + Box::new(|_, api| { + api.respond(Response::builder().body(Vec::new()).unwrap()); + }), + )); + self.platform_specific.with_asset_loader = true; + self.platform_specific.asset_loader_domain = Some(format!("{}.assets", protocol)); + self + } + + fn with_https_scheme(mut self, enabled: bool) -> Self { + self.platform_specific.https_scheme = enabled; + self + } +} + +/// The fundamental type to present a [`WebView`]. +/// +/// [`WebViewBuilder`] / [`WebView`] are the basic building blocks to construct WebView contents and +/// scripts for those who prefer to control fine grained window creation and event handling. +/// [`WebView`] presents the actual WebView window and let you still able to perform actions on it. +pub struct WebView { + webview: InnerWebView, +} + +impl WebView { + /// Create a [`WebView`] from from a type that implements [`HasWindowHandle`]. + /// Note that calling this directly loses + /// abilities to initialize scripts, add ipc handler, and many more before starting WebView. To + /// benefit from above features, create a [`WebViewBuilder`] instead. + /// + /// # Platform-specific: + /// + /// - **Linux**: Only X11 is supported, if you want to support Wayland too, use [`WebView::new_gtk`]. + /// + /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk + /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. + /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. + /// - **macOS / Windows**: The webview will auto-resize when the passed handle is resized. + /// - **Linux (X11)**: Unlike macOS and Windows, the webview will not auto-resize and you'll need to call [`WebView::set_size`] manually. + /// + /// # Panics: + /// + /// - Panics if the provided handle was not supported or invalid. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + pub fn new(window: &impl HasRawWindowHandle) -> Result { + WebViewBuilder::new(window).build() + } + + /// Create [`WebViewBuilder`] as a child window inside the provided [`HasRawWindowHandle`]. + /// + /// ## Platform-specific + /// + /// - **Windows**: This will create the webview as a child window of the `parent` window. + /// - **macOS**: This will create the webview as a `NSView` subview of the `parent` window's + /// content view. + /// - **Linux**: This will create the webview as a child window of the `parent` window. Only X11 + /// is supported. This method won't work on Wayland. + /// + /// Although this methods only needs an X11 window handle, you use webkit2gtk, so you still need to initialize gtk + /// by callling [`gtk::init`] and advance its loop alongside your event loop using [`gtk::main_iteration_do`]. + /// Checkout the [Platform Considerations](https://docs.rs/wry/latest/wry/#platform-considerations) section in the crate root documentation. + /// - **Android/iOS:** Unsupported. + /// + /// # Panics: + /// + /// - Panics if the provided handle was not support or invalid. + /// - Panics on Linux, if [`gtk::init`] was not called in this thread. + pub fn new_as_child(parent: &impl HasRawWindowHandle) -> Result { + WebViewBuilder::new_as_child(parent).build() + } + #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", - target_os = "openbsd" + target_os = "openbsd", ))] - #[error("Fail to fetch security manager")] - MissingManager, - #[error("Failed to initialize the script")] - InitScriptError, - #[error("Bad RPC request: {0} ((1))")] - RpcScriptError(String, String), - #[error(transparent)] - NulError(#[from] std::ffi::NulError), - #[error(transparent)] - OsError(#[from] crate::application::error::OsError), - #[error(transparent)] - ReceiverError(#[from] RecvError), - #[error(transparent)] - SenderError(#[from] SendError), - #[error("Failed to send the message")] - MessageSender, - #[error(transparent)] - Json(#[from] serde_json::Error), - #[error(transparent)] - UrlError(#[from] ParseError), - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("Icon error: {0}")] - Icon(#[from] BadIcon), - #[cfg(target_os = "windows")] - #[error("WebView2 error: {0}")] - WebView2Error(webview2_com::Error), - #[error("Duplicate custom protocol registered: {0}")] - DuplicateCustomProtocol(String), - #[error(transparent)] - HttpError(#[from] http::Error), - #[error("Infallible error, something went really wrong: {0}")] - Infallible(#[from] std::convert::Infallible), - #[cfg(target_os = "android")] - #[error(transparent)] - JniError(#[from] tao::platform::android::ndk_glue::jni::errors::Error), - #[error("Failed to create proxy endpoint")] - ProxyEndpointCreationFailed, + /// Create the webview from a GTK container widget, such as GTK window. + /// + /// # Panics: + /// + /// - Panics if [`gtk::init`] was not called in this thread. + pub fn new_gtk(widget: &W) -> Result + where + W: gtk::prelude::IsA, + { + WebViewBuilder::new_gtk(widget).build() + } + + /// Get the current url of the webview + pub fn url(&self) -> Url { + self.webview.url() + } + + /// Evaluate and run javascript code. + pub fn evaluate_script(&self, js: &str) -> Result<()> { + self + .webview + .eval(js, None::>) + } + + /// Evaluate and run javascript code with callback function. The evaluation result will be + /// serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on windows. You can catch it yourself and return as string as a workaround. + /// + /// - ** Android:** Not implemented yet. + pub fn evaluate_script_with_callback( + &self, + js: &str, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + self.webview.eval(js, Some(callback)) + } + + /// Launch print modal for the webview content. + pub fn print(&self) -> Result<()> { + self.webview.print(); + Ok(()) + } + + /// Open the web inspector which is usually called dev tool. + /// + /// ## Platform-specific + /// + /// - **Android / iOS:** Not supported. + #[cfg(any(debug_assertions, feature = "devtools"))] + pub fn open_devtools(&self) { + self.webview.open_devtools(); + } + + /// Close the web inspector which is usually called dev tool. + /// + /// ## Platform-specific + /// + /// - **Windows / Android / iOS:** Not supported. + #[cfg(any(debug_assertions, feature = "devtools"))] + pub fn close_devtools(&self) { + self.webview.close_devtools(); + } + + /// Gets the devtool window's current visibility state. + /// + /// ## Platform-specific + /// + /// - **Windows / Android / iOS:** Not supported. + #[cfg(any(debug_assertions, feature = "devtools"))] + pub fn is_devtools_open(&self) -> bool { + self.webview.is_devtools_open() + } + + /// Set the webview zoom level + /// + /// ## Platform-specific: + /// + /// - **Android**: Not supported. + /// - **macOS**: available on macOS 11+ only. + /// - **iOS**: available on iOS 14+ only. + pub fn zoom(&self, scale_factor: f64) { + self.webview.zoom(scale_factor); + } + + /// Specify the webview background color. + /// + /// The color uses the RGBA format. + /// + /// ## Platfrom-specific: + /// + /// - **macOS / iOS**: Not implemented. + /// - **Windows**: + /// - On Windows 7, transparency is not supported and the alpha value will be ignored. + /// - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + pub fn set_background_color(&self, background_color: RGBA) -> Result<()> { + self.webview.set_background_color(background_color) + } + + /// Navigate to the specified url + pub fn load_url(&self, url: &str) { + self.webview.load_url(url) + } + + /// Navigate to the specified url using the specified headers + pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) { + self.webview.load_url_with_headers(url, headers) + } + + /// Clear all browsing data + pub fn clear_all_browsing_data(&self) -> Result<()> { + self.webview.clear_all_browsing_data() + } + + /// Set the webview position relative to its parent if it was created as a child. + pub fn set_position(&self, position: (i32, i32)) { + self.webview.set_position(position) + } + + /// Set the webview size if it was created as a child + /// or if ot was created directly in an X11 Window. + pub fn set_size(&self, size: (u32, u32)) { + self.webview.set_size(size) + } + + /// Shows or hides the webview. + pub fn set_visible(&self, visible: bool) { + self.webview.set_visible(visible) + } + + /// Try moving focus to the webview. + pub fn focus(&self) { + self.webview.focus() + } +} + +/// An event describing the files drop on the webview. +#[non_exhaustive] +#[derive(Debug, serde::Serialize, Clone)] +pub enum FileDropEvent { + /// The file(s) have been dragged onto the window, but have not been dropped yet. + Hovered { + paths: Vec, + /// The position of the mouse cursor. + position: (i32, i32), + }, + /// The file(s) have been dropped onto the window. + Dropped { + paths: Vec, + /// The position of the mouse cursor. + position: (i32, i32), + }, + /// The file drop was aborted. + Cancelled, +} + +/// Get Webview/Webkit version on current platform. +pub fn webview_version() -> Result { + platform_webview_version() +} + +/// The [memory usage target level][1]. There are two levels 'Low' and 'Normal' and the default +/// level is 'Normal'. When the application is going inactive, setting the level to 'Low' can +/// significantly reduce the application's memory consumption. +/// +/// [1]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2memoryusagetargetlevel +#[cfg(target_os = "windows")] +#[non_exhaustive] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MemoryUsageLevel { + /// The 'Normal' memory usage. Applications should set this level when they are becoming active. + #[default] + Normal, + /// The 'Low' memory usage. Applications can reduce memory comsumption by setting this level when + /// they are becoming inactive. + Low, +} + +/// Additional methods on `WebView` that are specific to Windows. +#[cfg(target_os = "windows")] +pub trait WebviewExtWindows { + /// Returns WebView2 Controller + fn controller(&self) -> ICoreWebView2Controller; + + /// Changes the webview2 theme. + fn set_theme(&self, theme: Theme); + + /// Sets the [memory usage target level][1]. + /// + /// When to best use this mode depends on the app in question. Most commonly it's called when + /// the app's visiblity state changes. + /// + /// Please read the [guide for WebView2][2] for more details. + /// + /// This method uses a WebView2 API added in Runtime version 114.0.1823.32. When it is used in + /// an older Runtime version, it does nothing. + /// + /// [1]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2memoryusagetargetlevel + /// [2]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.memoryusagetargetlevel?view=webview2-dotnet-1.0.2088.41#remarks + fn set_memory_usage_level(&self, level: MemoryUsageLevel); +} + +#[cfg(target_os = "windows")] +impl WebviewExtWindows for WebView { + fn controller(&self) -> ICoreWebView2Controller { + self.webview.controller.clone() + } + + fn set_theme(&self, theme: Theme) { + self.webview.set_theme(theme) + } + + fn set_memory_usage_level(&self, level: MemoryUsageLevel) { + self.webview.set_memory_usage_level(level); + } +} + +/// Additional methods on `WebView` that are specific to Linux. +#[cfg(target_os = "linux")] +pub trait WebviewExtUnix { + /// Returns Webkit2gtk Webview handle + fn webview(&self) -> webkit2gtk::WebView; +} + +#[cfg(target_os = "linux")] +impl WebviewExtUnix for WebView { + fn webview(&self) -> webkit2gtk::WebView { + self.webview.webview.clone() + } +} + +/// Additional methods on `WebView` that are specific to macOS. +#[cfg(target_os = "macos")] +pub trait WebviewExtMacOS { + /// Returns WKWebView handle + fn webview(&self) -> cocoa::base::id; + /// Returns WKWebView manager [(userContentController)](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller) handle + fn manager(&self) -> cocoa::base::id; + /// Returns NSWindow associated with the WKWebView webview + fn ns_window(&self) -> cocoa::base::id; +} + +#[cfg(target_os = "macos")] +impl WebviewExtMacOS for WebView { + fn webview(&self) -> cocoa::base::id { + self.webview.webview + } + + fn manager(&self) -> cocoa::base::id { + self.webview.manager + } + + fn ns_window(&self) -> cocoa::base::id { + self.webview.ns_window + } +} + +/// Additional methods on `WebView` that are specific to iOS. +#[cfg(target_os = "ios")] +pub trait WebviewExtIOS { + /// Returns WKWebView handle + fn webview(&self) -> cocoa::base::id; + /// Returns WKWebView manager [(userContentController)](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller) handle + fn manager(&self) -> cocoa::base::id; +} + +#[cfg(target_os = "ios")] +impl WebviewExtIOS for WebView { + fn webview(&self) -> cocoa::base::id { + self.webview.webview + } + + fn manager(&self) -> cocoa::base::id { + self.webview.manager + } +} + +#[cfg(target_os = "android")] +/// Additional methods on `WebView` that are specific to Android +pub trait WebviewExtAndroid { + fn handle(&self) -> JniHandle; +} + +#[cfg(target_os = "android")] +impl WebviewExtAndroid for WebView { + fn handle(&self) -> JniHandle { + JniHandle + } +} + +/// Webview theme. +#[derive(Debug, Clone, Copy)] +pub enum Theme { + /// Dark + Dark, + /// Light + Light, + /// System preference + Auto, +} + +/// Type alias for a color in the RGBA format. +/// +/// Each value can be 0..255 inclusive. +pub type RGBA = (u8, u8, u8, u8); + +/// Type of of page loading event +pub enum PageLoadEvent { + /// Indicates that the content of the page has started loading + Started, + /// Indicates that the page content has finished loading + Finished, +} + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "ios", + target_os = "macos", +))] +#[derive(Default)] +pub(crate) struct PlatformSpecificWebViewAttributes; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_get_webview_version() { + if let Err(error) = webview_version() { + panic!("{}", error); + } + } } diff --git a/src/webview/proxy.rs b/src/proxy.rs similarity index 100% rename from src/webview/proxy.rs rename to src/proxy.rs diff --git a/src/webview/web_context.rs b/src/web_context.rs similarity index 97% rename from src/webview/web_context.rs rename to src/web_context.rs index 216b7b5a2..2266b350a 100644 --- a/src/webview/web_context.rs +++ b/src/web_context.rs @@ -9,7 +9,7 @@ target_os = "netbsd", target_os = "openbsd" ))] -use crate::webview::webkitgtk::WebContextImpl; +use crate::webkitgtk::WebContextImpl; use std::path::{Path, PathBuf}; @@ -23,7 +23,7 @@ use std::path::{Path, PathBuf}; /// some actions like custom protocol on Mac. Please keep both instances when you still wish to /// interact with them. /// -/// [`WebView`]: crate::webview::WebView +/// [`WebView`]: crate::WebView #[derive(Debug)] pub struct WebContext { data: WebContextData, diff --git a/src/webview/webkitgtk/file_drop.rs b/src/webkitgtk/file_drop.rs similarity index 59% rename from src/webview/webkitgtk/file_drop.rs rename to src/webkitgtk/file_drop.rs index 22845bbcd..cb9eab121 100644 --- a/src/webview/webkitgtk/file_drop.rs +++ b/src/webkitgtk/file_drop.rs @@ -4,23 +4,15 @@ use std::{cell::Cell, path::PathBuf, rc::Rc}; -use gtk::prelude::*; +use gtk::{glib, prelude::*}; use webkit2gtk::WebView; -use crate::{ - application::{dpi::LogicalPosition, window::Window}, - webview::FileDropEvent, -}; +use crate::FileDropEvent; -pub(crate) fn connect_drag_event( - webview: Rc, - window: Rc, - handler: Box bool>, -) { +pub(crate) fn connect_drag_event(webview: WebView, handler: Box bool>) { let listener = Rc::new((handler, Cell::new(None))); let listener_ref = listener.clone(); - let w = window.clone(); webview.connect_drag_data_received(move |_, _, x, y, data, info, _| { if info == 2 { let uris = data @@ -33,47 +25,33 @@ pub(crate) fn connect_drag_event( .collect::>(); listener_ref.1.set(Some(uris.clone())); - let scale_factor = w.scale_factor(); - let position = LogicalPosition::new(x, y).to_physical(scale_factor); - - listener_ref.0( - &w, - FileDropEvent::Hovered { - paths: uris, - position, - }, - ); + listener_ref.0(FileDropEvent::Hovered { + paths: uris, + position: (x, y), + }); } else { // drag_data_received is called twice, so we can ignore this signal } }); let listener_ref = listener.clone(); - let w = window.clone(); webview.connect_drag_drop(move |_, _, x, y, _| { let uris = listener_ref.1.take(); if let Some(uris) = uris { - let scale_factor = w.scale_factor(); - let position = LogicalPosition::new(x, y).to_physical(scale_factor); - - listener_ref.0( - &w, - FileDropEvent::Dropped { - paths: uris, - position, - }, - ) + listener_ref.0(FileDropEvent::Dropped { + paths: uris, + position: (x, y), + }) } else { false } }); let listener_ref = listener.clone(); - let w = window.clone(); webview.connect_drag_leave(move |_, _, time| { if time == 0 { // The user cancelled the drag n drop - listener_ref.0(&w, FileDropEvent::Cancelled); + listener_ref.0(FileDropEvent::Cancelled); } else { // The user dropped the file on the window, but this will be handled in connect_drag_drop instead } @@ -81,10 +59,9 @@ pub(crate) fn connect_drag_event( // Called when a drag "fails" - we'll just emit a Cancelled event. let listener_ref = listener.clone(); - let w = window; webview.connect_drag_failed(move |_, _, _| { - if listener_ref.0(&w, FileDropEvent::Cancelled) { - gtk::glib::Propagation::Stop + if listener_ref.0(FileDropEvent::Cancelled) { + glib::Propagation::Stop } else { gtk::glib::Propagation::Proceed } diff --git a/src/webview/webkitgtk/mod.rs b/src/webkitgtk/mod.rs similarity index 68% rename from src/webview/webkitgtk/mod.rs rename to src/webkitgtk/mod.rs index 999975238..ba34662b4 100644 --- a/src/webview/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -2,15 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use gtk::{gdk::EventMask, gio::Cancellable, prelude::*}; +use gdkx11::{ + glib::translate::{FromGlibPtrFull, ToGlibPtr}, + X11Display, +}; +use gtk::{ + gdk::{self, EventMask}, + gio::Cancellable, + prelude::*, +}; +use javascriptcore::ValueExt; +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; #[cfg(any(debug_assertions, feature = "devtools"))] use std::sync::atomic::{AtomicBool, Ordering}; -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, - rc::Rc, - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, Mutex}; use url::Url; use webkit2gtk::{ AutoplayPolicy, InputMethodContextExt, LoadEvent, NavigationPolicyDecision, @@ -23,39 +28,156 @@ use webkit2gtk_sys::{ webkit_get_major_version, webkit_get_micro_version, webkit_get_minor_version, webkit_policy_decision_ignore, webkit_policy_decision_use, }; +use x11_dl::xlib::*; use web_context::WebContextExt; pub use web_context::WebContextImpl; use crate::{ - application::{platform::unix::*, window::Window}, - webview::{proxy::ProxyConfig, web_context::WebContext, PageLoadEvent, WebViewAttributes, RGBA}, - Error, Result, + proxy::ProxyConfig, web_context::WebContext, Error, PageLoadEvent, Result, WebViewAttributes, + RGBA, }; mod file_drop; mod synthetic_mouse_events; -mod undecorated_resizing; mod web_context; -use javascriptcore::ValueExt; - pub(crate) struct InnerWebView { - pub webview: Rc, + pub webview: WebView, #[cfg(any(debug_assertions, feature = "devtools"))] is_inspector_open: Arc, pending_scripts: Arc>>>, + + is_child: bool, + xlib: Option, + x11_display: Option<*mut std::ffi::c_void>, + x11_window: Option, + display: Option, + gtk_window: Option, +} + +impl Drop for InnerWebView { + fn drop(&mut self) { + unsafe { self.webview.destroy() } + + if let Some(xlib) = &self.xlib { + if self.is_child { + unsafe { (xlib.XDestroyWindow)(self.x11_display.unwrap() as _, self.x11_window.unwrap()) }; + } + } + + if let Some(window) = &self.gtk_window { + window.close(); + } + } } impl InnerWebView { - pub fn new( - window: Rc, + pub fn new( + window: &W, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + web_context: Option<&mut WebContext>, + ) -> Result { + Self::new_x11(window, attributes, pl_attrs, web_context, false) + } + + pub fn new_as_child( + parent: &W, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + web_context: Option<&mut WebContext>, + ) -> Result { + Self::new_x11(parent, attributes, pl_attrs, web_context, true) + } + + fn new_x11( + window: &W, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + web_context: Option<&mut WebContext>, + is_child: bool, + ) -> Result { + let xlib = Xlib::open()?; + + let window_handle = match window.raw_window_handle() { + RawWindowHandle::Xlib(w) => w.window, + _ => return Err(Error::UnsupportedWindowHandle), + }; + + let gdk_display = gdk::Display::default().ok_or(Error::X11DisplayNotFound)?; + let gx11_display: &X11Display = gdk_display.downcast_ref().unwrap(); + let raw = gx11_display.to_glib_none().0; + let display = unsafe { gdkx11::ffi::gdk_x11_display_get_xdisplay(raw) }; + + let window = if is_child { + let child = unsafe { + (xlib.XCreateSimpleWindow)( + display as _, + window_handle, + attributes.position.map(|p| p.0).unwrap_or(0), + attributes.position.map(|p| p.1).unwrap_or(0), + attributes.size.map(|s| s.0).unwrap_or(0), + attributes.size.map(|s| s.1).unwrap_or(0), + 0, + 0, + 0, + ) + }; + if attributes.visible { + unsafe { (xlib.XMapWindow)(display as _, child) }; + } + child + } else { + window_handle + }; + + let gdk_window = unsafe { + let raw = gdkx11::ffi::gdk_x11_window_foreign_new_for_display(raw, window); + gdk::Window::from_glib_full(raw) + }; + let gtk_window = gtk::Window::new(gtk::WindowType::Toplevel); + gtk_window.connect_realize(move |widget| widget.set_window(gdk_window.clone())); + gtk_window.set_has_window(true); + gtk_window.realize(); + + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + gtk_window.add(&vbox); + + let hidden = !attributes.visible; + + Self::new_gtk(&vbox, attributes, pl_attrs, web_context).map(|mut w| { + // for some reason, if the webview starts as hidden, + // we will need about 3 calls to `webview.set_visible` + // with alternating value. + // calling gtk_window.show_all() then hiding it again + // seems to fix the issue. + gtk_window.show_all(); + if hidden { + gtk_window.hide(); + } + + w.is_child = is_child; + w.xlib = Some(xlib); + w.display = Some(gdk_display); + w.x11_display = Some(display as _); + w.x11_window = Some(window); + w.gtk_window = Some(gtk_window); + + w + }) + } + + pub fn new_gtk( + container: &W, mut attributes: WebViewAttributes, _pl_attrs: super::PlatformSpecificWebViewAttributes, web_context: Option<&mut WebContext>, - ) -> Result { - let window_rc = Rc::clone(&window); - let window = &window.gtk_window(); + ) -> Result + where + W: IsA, + { + let window_id = container.as_ptr() as isize; // default_context allows us to create a scoped context on-demand let mut default_context; @@ -108,44 +230,25 @@ impl InnerWebView { web_context.register_automation(webview.clone()); // Message handler - let webview = Rc::new(webview); - let w = window_rc.clone(); let ipc_handler = attributes.ipc_handler.take(); let manager = web_context.manager(); - // Use the window hash as the script handler name to prevent from conflict when sharing same - // web context. - let window_hash = { - let mut hasher = DefaultHasher::new(); - w.id().hash(&mut hasher); - hasher.finish().to_string() - }; // Connect before registering as recommended by the docs manager.connect_script_message_received(None, move |_m, msg| { if let Some(js) = msg.js_value() { if let Some(ipc_handler) = &ipc_handler { - ipc_handler(&w, js.to_string()); + ipc_handler(js.to_string()); } } }); // Register the handler we just connected - manager.register_script_message_handler(&window_hash); - - // Allow the webview to close it's own window - let close_window = window_rc.clone(); - webview.connect_close(move |_| { - close_window.gtk_window().close(); - }); + manager.register_script_message_handler(&window_id.to_string()); // document title changed handler if let Some(document_title_changed_handler) = attributes.document_title_changed_handler { - let w = window_rc.clone(); webview.connect_title_notify(move |webview| { - document_title_changed_handler( - &w, - webview.title().map(|t| t.to_string()).unwrap_or_default(), - ) + document_title_changed_handler(webview.title().map(|t| t.to_string()).unwrap_or_default()) }); } @@ -174,7 +277,6 @@ impl InnerWebView { ); synthetic_mouse_events::setup(&webview); - undecorated_resizing::setup(&webview); if attributes.navigation_handler.is_some() || attributes.new_window_req_handler.is_some() { webview.connect_decide_policy(move |_webview, policy_decision, policy_type| { @@ -216,24 +318,30 @@ impl InnerWebView { ) } - // tao adds a default vertical box so we check for that first - if let Some(vbox) = window_rc.default_vbox() { - vbox.pack_start(&*webview, true, true, 0); + if container.type_().name() == "GtkBox" { + container + .dynamic_cast_ref::() + .unwrap() + .pack_start(&webview, true, true, 0); } else { - window.add(&*webview); + container.add(&webview); + } + + if attributes.visible { + webview.show_all(); } if attributes.focused { webview.grab_focus(); } - if let Some(context) = WebViewExt::context(&*webview) { + if let Some(context) = WebViewExt::context(&webview) { use webkit2gtk::WebContextExt; context.set_use_system_appearance_for_scrollbars(false); } // Enable webgl, webaudio, canvas features as default. - if let Some(settings) = WebViewExt::settings(&*webview) { + if let Some(settings) = WebViewExt::settings(&webview) { settings.set_enable_webgl(true); settings.set_enable_webaudio(true); settings @@ -273,17 +381,13 @@ impl InnerWebView { // File drop handling if let Some(file_drop_handler) = attributes.file_drop_handler { - file_drop::connect_drag_event(webview.clone(), window_rc, file_drop_handler); - } - - if window.get_visible() { - window.show_all(); + file_drop::connect_drag_event(webview.clone(), file_drop_handler); } #[cfg(any(debug_assertions, feature = "devtools"))] let is_inspector_open = { let is_inspector_open = Arc::new(AtomicBool::default()); - if let Some(inspector) = WebViewExt::inspector(&*webview) { + if let Some(inspector) = WebViewExt::inspector(&webview) { let is_inspector_open_ = is_inspector_open.clone(); inspector.connect_bring_to_front(move |_| { is_inspector_open_.store(true, Ordering::Relaxed); @@ -302,12 +406,18 @@ impl InnerWebView { #[cfg(any(debug_assertions, feature = "devtools"))] is_inspector_open, pending_scripts: Arc::new(Mutex::new(Some(Vec::new()))), + is_child: false, + xlib: None, + display: None, + x11_display: None, + x11_window: None, + gtk_window: None, }; // Initialize message handler let mut init = String::with_capacity(115 + 20 + 22); init.push_str("Object.defineProperty(window, 'ipc', {value: Object.freeze({postMessage:function(x){window.webkit.messageHandlers[\""); - init.push_str(&window_hash); + init.push_str(&window_id.to_string()); init.push_str("\"].postMessage(x)}})})"); w.init(&init)?; @@ -328,7 +438,7 @@ impl InnerWebView { // Navigation if let Some(url) = attributes.url { - web_context.queue_load_uri(Rc::clone(&w.webview), url, attributes.headers); + web_context.queue_load_uri(w.webview.clone(), url, attributes.headers); web_context.flush_queue_loader(); } else if let Some(html) = attributes.html { w.webview.load_html(&html, None); @@ -418,7 +528,7 @@ impl InnerWebView { #[cfg(any(debug_assertions, feature = "devtools"))] pub fn open_devtools(&self) { - if let Some(inspector) = WebViewExt::inspector(&*self.webview) { + if let Some(inspector) = WebViewExt::inspector(&self.webview) { inspector.show(); // `bring-to-front` is not received in this case self.is_inspector_open.store(true, Ordering::Relaxed); @@ -427,7 +537,7 @@ impl InnerWebView { #[cfg(any(debug_assertions, feature = "devtools"))] pub fn close_devtools(&self) { - if let Some(inspector) = WebViewExt::inspector(&*self.webview) { + if let Some(inspector) = WebViewExt::inspector(&self.webview) { inspector.close(); } } @@ -438,7 +548,7 @@ impl InnerWebView { } pub fn zoom(&self, scale_factor: f64) { - WebViewExt::set_zoom_level(&*self.webview, scale_factor); + WebViewExt::set_zoom_level(&self.webview, scale_factor); } pub fn set_background_color(&self, background_color: RGBA) -> Result<()> { @@ -471,8 +581,8 @@ impl InnerWebView { } pub fn clear_all_browsing_data(&self) -> Result<()> { - if let Some(context) = WebViewExt::context(&*self.webview) { - use webkit2gtk::WebContextExt; + use webkit2gtk::WebContextExt; + if let Some(context) = WebViewExt::context(&self.webview) { if let Some(data_manger) = context.website_data_manager() { webkit2gtk::WebsiteDataManagerExtManual::clear( &data_manger, @@ -486,6 +596,52 @@ impl InnerWebView { Ok(()) } + + pub fn set_position(&self, position: (i32, i32)) { + if self.is_child { + if let Some(window) = &self.gtk_window { + window.move_(position.0, position.1); + } + } + } + + pub fn set_size(&self, size: (u32, u32)) { + if let Some(window) = &self.gtk_window { + if self.is_child { + window.window().unwrap().resize(size.0 as _, size.1 as _); + } + window.size_allocate(>k::Allocation::new(200, 200, size.0 as _, size.1 as _)); + } + } + + pub fn set_visible(&self, visible: bool) { + if self.is_child { + let xlib = self.xlib.as_ref().unwrap(); + if visible { + unsafe { (xlib.XMapWindow)(self.x11_display.unwrap() as _, self.x11_window.unwrap()) }; + } else { + unsafe { (xlib.XUnmapWindow)(self.x11_display.unwrap() as _, self.x11_window.unwrap()) }; + } + } + + if visible { + self.webview.show_all(); + } else { + self.webview.hide(); + } + + if let Some(window) = &self.gtk_window { + if visible { + window.show_all(); + } else { + window.hide(); + } + } + } + + pub fn focus(&self) { + self.webview.grab_focus(); + } } pub fn platform_webview_version() -> Result { diff --git a/src/webview/webkitgtk/synthetic_mouse_events.rs b/src/webkitgtk/synthetic_mouse_events.rs similarity index 100% rename from src/webview/webkitgtk/synthetic_mouse_events.rs rename to src/webkitgtk/synthetic_mouse_events.rs diff --git a/src/webview/webkitgtk/web_context.rs b/src/webkitgtk/web_context.rs similarity index 95% rename from src/webview/webkitgtk/web_context.rs rename to src/webkitgtk/web_context.rs index eb68acf73..7b54bbdb9 100644 --- a/src/webview/webkitgtk/web_context.rs +++ b/src/webkitgtk/web_context.rs @@ -4,11 +4,8 @@ //! Unix platform extensions for [`WebContext`](super::WebContext). -use crate::{ - webview::{web_context::WebContextData, RequestAsyncResponder}, - Error, -}; -use gtk::glib::FileError; +use crate::{web_context::WebContextData, Error, RequestAsyncResponder}; +use gtk::glib; use http::{header::CONTENT_TYPE, Request, Response as HttpResponse}; use std::{ borrow::Cow, @@ -136,7 +133,7 @@ pub trait WebContextExt { /// Add a [`WebView`] to the queue waiting to be opened. /// /// See the `WebviewUriLoader` for more information. - fn queue_load_uri(&self, webview: Rc, url: Url, headers: Option); + fn queue_load_uri(&self, webview: WebView, url: Url, headers: Option); /// Flush all queued [`WebView`]s waiting to load a uri. /// @@ -189,7 +186,7 @@ impl WebContextExt for super::WebContext { } } - fn queue_load_uri(&self, webview: Rc, url: Url, headers: Option) { + fn queue_load_uri(&self, webview: WebView, url: Url, headers: Option) { self.os.webview_uri_loader.push(webview, url, headers) } @@ -328,11 +325,7 @@ where #[cfg(feature = "linux-body")] { - use gtk::{ - gdk::prelude::{InputStreamExt, InputStreamExtManual}, - gio::Cancellable, - glib::Bytes, - }; + use gtk::{gdk::prelude::InputStreamExtManual, gio::Cancellable}; // Set request http body let cancellable: Option<&Cancellable> = None; @@ -354,7 +347,7 @@ where } result }) - .unwrap_or(Vec::new()); + .unwrap_or_default(); } #[cfg(not(feature = "linux-body"))] { @@ -366,7 +359,7 @@ where Err(_) => { request.finish_error(&mut gtk::glib::Error::new( // TODO: use UriError when we can use 2_66 webkit2gtk feature flag - FileError::Exist, + glib::FileError::Exist, "Could not get uri.", )); return; @@ -402,8 +395,8 @@ where handler(http_request, RequestAsyncResponder { responder }); } else { - request.finish_error(&mut gtk::glib::Error::new( - FileError::Exist, + request.finish_error(&mut glib::Error::new( + glib::FileError::Exist, "Could not get uri.", )); } @@ -443,7 +436,7 @@ where #[derive(Debug, Default)] struct WebviewUriLoader { lock: AtomicBool, - queue: Mutex, Url, Option)>>, + queue: Mutex)>>, } impl WebviewUriLoader { @@ -458,13 +451,13 @@ impl WebviewUriLoader { } /// Add a [`WebView`] to the queue. - fn push(&self, webview: Rc, url: Url, headers: Option) { + fn push(&self, webview: WebView, url: Url, headers: Option) { let mut queue = self.queue.lock().expect("poisoned load queue"); queue.push_back((webview, url, headers)) } /// Remove a [`WebView`] from the queue and return it. - fn pop(&self) -> Option<(Rc, Url, Option)> { + fn pop(&self) -> Option<(WebView, Url, Option)> { let mut queue = self.queue.lock().expect("poisoned load queue"); queue.pop_front() } diff --git a/src/webview/mod.rs b/src/webview/mod.rs deleted file mode 100644 index fdc0e8bba..000000000 --- a/src/webview/mod.rs +++ /dev/null @@ -1,1283 +0,0 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! [`WebView`] struct and associated types. - -mod proxy; -mod web_context; - -pub use web_context::WebContext; - -#[cfg(target_os = "android")] -pub(crate) mod android; -#[cfg(target_os = "android")] -pub mod prelude { - pub use super::android::{binding::*, dispatch, find_class, setup, Context}; -} -#[cfg(target_os = "android")] -pub use android::JniHandle; -#[cfg(target_os = "android")] -use android::*; -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -pub(crate) mod webkitgtk; -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -use webkitgtk::*; -#[cfg(any(target_os = "macos", target_os = "ios"))] -pub(crate) mod wkwebview; -#[cfg(any(target_os = "macos", target_os = "ios"))] -use wkwebview::*; -#[cfg(target_os = "windows")] -pub(crate) mod webview2; -#[cfg(target_os = "windows")] -use self::webview2::*; -use crate::{application::dpi::PhysicalPosition, Result}; -#[cfg(target_os = "windows")] -use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller; -#[cfg(target_os = "windows")] -use windows::{Win32::Foundation::HWND, Win32::UI::WindowsAndMessaging::DestroyWindow}; - -use std::{borrow::Cow, path::PathBuf, rc::Rc}; - -pub use proxy::{ProxyConfig, ProxyEndpoint}; -pub use url::Url; - -#[cfg(target_os = "windows")] -use crate::application::platform::windows::WindowExtWindows; -use crate::application::{dpi::PhysicalSize, window::Window}; - -use http::{Request, Response as HttpResponse}; - -/// Resolves a custom protocol [`Request`] asynchronously. -/// -/// See [`WebViewBuilder::with_asynchronous_custom_protocol`] for more information. -pub struct RequestAsyncResponder { - pub(crate) responder: Box>)>, -} - -// SAFETY: even though the webview bindings do not indicate the responder is Send, -// it actually is and we need it in order to let the user do the protocol computation -// on a separate thread or async task. -unsafe impl Send for RequestAsyncResponder {} - -impl RequestAsyncResponder { - /// Resolves the request with the given response. - pub fn respond>>(self, response: HttpResponse) { - let (parts, body) = response.into_parts(); - (self.responder)(HttpResponse::from_parts(parts, body.into())) - } -} - -pub struct WebViewAttributes { - /// Whether the WebView should have a custom user-agent. - pub user_agent: Option, - /// Whether the WebView window should be visible. - pub visible: bool, - /// Whether the WebView should be transparent. - /// - /// ## Platform-specific: - /// - /// **Windows 7**: Not supported. - pub transparent: bool, - /// Specify the webview background color. This will be ignored if `transparent` is set to `true`. - /// - /// The color uses the RGBA format. - /// - /// ## Platform-specific: - /// - /// - **macOS / iOS**: Not implemented. - /// - **Windows**: - /// - On Windows 7, transparency is not supported and the alpha value will be ignored. - /// - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` - pub background_color: Option, - /// Whether load the provided URL to [`WebView`]. - pub url: Option, - /// Headers used when loading the requested `url`. - pub headers: Option, - /// Whether page zooming by hotkeys is enabled - /// - /// ## Platform-specific - /// - /// **macOS / Linux / Android / iOS**: Unsupported - pub zoom_hotkeys_enabled: bool, - /// Whether load the provided html string to [`WebView`]. - /// This will be ignored if the `url` is provided. - /// - /// # Warning - /// - /// The Page loaded from html string will have `null` origin. - /// - /// ## PLatform-specific: - /// - /// - **Windows:** the string can not be larger than 2 MB (2 * 1024 * 1024 bytes) in total size - pub html: Option, - /// Initialize javascript code when loading new pages. When webview load a new page, this - /// initialization code will be executed. It is guaranteed that code is executed before - /// `window.onload`. - /// - /// ## Platform-specific - /// - /// - **Android:** The Android WebView does not provide an API for initialization scripts, - /// so we prepend them to each HTML head. They are only implemented on custom protocol URLs. - pub initialization_scripts: Vec, - /// Register custom file loading protocols with pairs of scheme uri string and a handling - /// closure. - /// - /// The closure takes a [Request] and returns a [Response]. - /// - /// # Warning - /// - /// Pages loaded from custom protocol will have different Origin on different platforms. And - /// servers which enforce CORS will need to add exact same Origin header in `Access-Control-Allow-Origin` - /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the - /// different Origin headers across platforms: - /// - /// - macOS, iOS and Linux: `://` (so it will be `wry://examples` in `custom_protocol` example). On Linux, You need to enable `linux-headers` feature flag. - /// - Windows and Android: `http://.` by default (so it will be `http://wry.examples` in `custom_protocol` example). To use `https` instead of `http`, use [`WebViewBuilderExtWindows::with_https_scheme`] and [`WebViewBuilderExtAndroid::with_https_scheme`]. - /// - /// # Reading assets on mobile - /// - /// - Android: Android has `assets` and `resource` path finder to - /// locate your files in those directories. For more information, see [Loading in-app content](https://developer.android.com/guide/webapps/load-local-content) page. - /// - iOS: To get the path of your assets, you can call [`CFBundle::resources_path`](https://docs.rs/core-foundation/latest/core_foundation/bundle/struct.CFBundle.html#method.resources_path). So url like `wry://assets/index.html` could get the html file in assets directory. - /// - /// [bug]: https://bugs.webkit.org/show_bug.cgi?id=229034 - pub custom_protocols: Vec<(String, Box>, RequestAsyncResponder)>)>, - /// Set the IPC handler to receive the message from Javascript on webview to host Rust code. - /// The message sent from webview should call `window.ipc.postMessage("insert_message_here");`. - /// - /// Both functions return promises but `notify()` resolves immediately. - pub ipc_handler: Option>, - /// Set a handler closure to process incoming [`FileDropEvent`] of the webview. - /// - /// # Blocking OS Default Behavior - /// Return `true` in the callback to block the OS' default behavior of handling a file drop. - /// - /// Note, that if you do block this behavior, it won't be possible to drop files on `` forms. - /// Also note, that it's not possible to manually set the value of a `` via JavaScript for security reasons. - #[cfg(feature = "file-drop")] - pub file_drop_handler: Option bool>>, - #[cfg(not(feature = "file-drop"))] - file_drop_handler: Option bool>>, - - /// Set a navigation handler to decide if incoming url is allowed to navigate. - /// - /// The closure take a `String` parameter as url and return `bool` to determine the url. True is - /// allow to navigate and false is not. - pub navigation_handler: Option bool>>, - - /// Set a download started handler to manage incoming downloads. - /// - /// The closure takes two parameters - the first is a `String` representing the url being downloaded from and and the - /// second is a mutable `PathBuf` reference that (possibly) represents where the file will be downloaded to. The latter - /// parameter can be used to set the download location by assigning a new path to it - the assigned path _must_ be - /// absolute. The closure returns a `bool` to allow or deny the download. - pub download_started_handler: Option bool>>, - - /// Sets a download completion handler to manage downloads that have finished. - /// - /// The closure is fired when the download completes, whether it was successful or not. - /// The closure takes a `String` representing the URL of the original download request, an `Option` - /// potentially representing the filesystem path the file was downloaded to, and a `bool` indicating if the download - /// succeeded. A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download - /// did not succeed, and may instead indicate some other failure - always check the third parameter if you need to - /// know if the download succeeded. - /// - /// ## Platform-specific: - /// - /// - **macOS**: The second parameter indicating the path the file was saved to is always empty, due to API - /// limitations. - pub download_completed_handler: Option, bool) + 'static>>, - - /// Set a new window handler to decide if incoming url is allowed to open in a new window. - /// - /// The closure take a `String` parameter as url and return `bool` to determine the url. True is - /// allow to navigate and false is not. - pub new_window_req_handler: Option bool>>, - - /// Enables clipboard access for the page rendered on **Linux** and **Windows**. - /// - /// macOS doesn't provide such method and is always enabled by default. But you still need to add menu - /// item accelerators to use shortcuts. - pub clipboard: bool, - - /// Enable web inspector which is usually called dev tool. - /// - /// Note this only enables dev tool to the webview. To open it, you can call - /// [`WebView::open_devtools`], or right click the page and open it from the context menu. - /// - /// ## Platform-specific - /// - /// - macOS: This will call private functions on **macOS**. It's still enabled if set in **debug** build on mac, - /// but requires `devtools` feature flag to actually enable it in **release** build. - /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. - /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. - pub devtools: bool, - /// Whether clicking an inactive window also clicks through to the webview. Default is `false`. - /// - /// ## Platform-specific - /// - /// This configuration only impacts macOS. - pub accept_first_mouse: bool, - - /// Indicates whether horizontal swipe gestures trigger backward and forward page navigation. - /// - /// ## Platform-specific: - /// - /// - **Android / iOS:** Unsupported. - pub back_forward_navigation_gestures: bool, - - /// Set a handler closure to process the change of the webview's document title. - pub document_title_changed_handler: Option>, - - /// Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is - /// enabled. - /// - /// ## Platform-specific: - /// - /// - **Android:** Unsupported yet. - pub incognito: bool, - - /// Whether all media can be played without user interaction. - pub autoplay: bool, - - /// Set a handler closure to process page load events. - pub on_page_load_handler: Option>, - - /// Set a proxy configuration for the webview. Supports HTTP CONNECT and SOCKSv5 proxies - /// - /// - **macOS**: Requires macOS 14.0+ and the `mac-proxy` feature flag to be enabled. - /// - **Android / iOS:** Not supported. - pub proxy_config: Option, - - /// Whether the webview should be focused when created. - /// - /// ## Platform-specific: - /// - /// - **macOS / Android / iOS:** Unsupported. - pub focused: bool, -} - -impl Default for WebViewAttributes { - fn default() -> Self { - Self { - user_agent: None, - visible: true, - transparent: false, - background_color: None, - url: None, - headers: None, - html: None, - initialization_scripts: vec![], - custom_protocols: vec![], - ipc_handler: None, - file_drop_handler: None, - navigation_handler: None, - download_started_handler: None, - download_completed_handler: None, - new_window_req_handler: None, - clipboard: false, - #[cfg(debug_assertions)] - devtools: true, - #[cfg(not(debug_assertions))] - devtools: false, - zoom_hotkeys_enabled: false, - accept_first_mouse: false, - back_forward_navigation_gestures: false, - document_title_changed_handler: None, - incognito: false, - autoplay: true, - on_page_load_handler: None, - proxy_config: None, - focused: true, - } - } -} - -#[cfg(windows)] -#[derive(Clone)] -pub(crate) struct PlatformSpecificWebViewAttributes { - additional_browser_args: Option, - browser_accelerator_keys: bool, - theme: Option, - https_scheme: bool, -} -#[cfg(windows)] -impl Default for PlatformSpecificWebViewAttributes { - fn default() -> Self { - Self { - additional_browser_args: None, - browser_accelerator_keys: true, // This is WebView2's default behavior - theme: None, - https_scheme: false, // To match macOS & Linux behavior in the context of mixed content. - } - } -} -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "macos", - target_os = "ios", -))] -#[derive(Default)] -pub(crate) struct PlatformSpecificWebViewAttributes; - -#[cfg(target_os = "android")] -#[derive(Default)] -pub(crate) struct PlatformSpecificWebViewAttributes { - on_webview_created: Option< - Box< - dyn Fn( - prelude::Context, - ) -> std::result::Result<(), tao::platform::android::ndk_glue::jni::errors::Error> - + Send, - >, - >, - with_asset_loader: bool, - asset_loader_domain: Option, - https_scheme: bool, -} - -/// Type alias for a color in the RGBA format. -/// -/// Each value can be 0..255 inclusive. -pub type RGBA = (u8, u8, u8, u8); - -/// Type of of page loading event -pub enum PageLoadEvent { - /// Indicates that the content of the page has started loading - Started, - /// Indicates that the page content has finished loading - Finished, -} - -/// Builder type of [`WebView`]. -/// -/// [`WebViewBuilder`] / [`WebView`] are the basic building blocks to construct WebView contents and -/// scripts for those who prefer to control fine grained window creation and event handling. -/// [`WebViewBuilder`] provides ability to setup initialization before web engine starts. -pub struct WebViewBuilder<'a> { - pub webview: WebViewAttributes, - platform_specific: PlatformSpecificWebViewAttributes, - web_context: Option<&'a mut WebContext>, - window: Window, -} - -impl<'a> WebViewBuilder<'a> { - /// Create [`WebViewBuilder`] from provided [`Window`]. - pub fn new(window: Window) -> Result { - let webview = WebViewAttributes::default(); - let web_context = None; - #[allow(clippy::default_constructed_unit_structs)] - let platform_specific = PlatformSpecificWebViewAttributes::default(); - - Ok(Self { - webview, - web_context, - window, - platform_specific, - }) - } - - /// Indicates whether horizontal swipe gestures trigger backward and forward page navigation. - /// - /// ## Platform-specific: - /// - /// - **Android / iOS:** Unsupported. - pub fn with_back_forward_navigation_gestures(mut self, gesture: bool) -> Self { - self.webview.back_forward_navigation_gestures = gesture; - self - } - - /// Sets whether the WebView should be transparent. - /// - /// ## Platform-specific: - /// - /// **Windows 7**: Not supported. - pub fn with_transparent(mut self, transparent: bool) -> Self { - self.webview.transparent = transparent; - self - } - - /// Specify the webview background color. This will be ignored if `transparent` is set to `true`. - /// - /// The color uses the RGBA format. - /// - /// ## Platfrom-specific: - /// - /// - **macOS / iOS**: Not implemented. - /// - **Windows**: - /// - on Windows 7, transparency is not supported and the alpha value will be ignored. - /// - on Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` - pub fn with_background_color(mut self, background_color: RGBA) -> Self { - self.webview.background_color = Some(background_color); - self - } - - /// Sets whether the WebView should be transparent. - pub fn with_visible(mut self, visible: bool) -> Self { - self.webview.visible = visible; - self - } - - /// Sets whether all media can be played without user interaction. - pub fn with_autoplay(mut self, autoplay: bool) -> Self { - self.webview.autoplay = autoplay; - self - } - - /// Initialize javascript code when loading new pages. When webview load a new page, this - /// initialization code will be executed. It is guaranteed that code is executed before - /// `window.onload`. - /// - /// ## Platform-specific - /// - /// - **Android:** The Android WebView does not provide an API for initialization scripts, - /// so we prepend them to each HTML head. They are only implemented on custom protocol URLs. - pub fn with_initialization_script(mut self, js: &str) -> Self { - if !js.is_empty() { - self.webview.initialization_scripts.push(js.to_string()); - } - self - } - - /// Register custom file loading protocols with pairs of scheme uri string and a handling - /// closure. - /// - /// The closure takes a [Request] and returns a [Response] - /// - /// # Warning - /// - /// Pages loaded from custom protocol will have different Origin on different platforms. And - /// servers which enforce CORS will need to add exact same Origin header in `Access-Control-Allow-Origin` - /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the - /// different Origin headers across platforms: - /// - /// - macOS, iOS and Linux: `://` (so it will be `wry://examples` in `custom_protocol` example). On Linux, You need to enable `linux-headers` feature flag. - /// - Windows and Android: `http://.` by default (so it will be `http://wry.examples` in `custom_protocol` example). To use `https` instead of `http`, use [`WebViewBuilderExtWindows::with_https_scheme`] and [`WebViewBuilderExtAndroid::with_https_scheme`]. - /// - /// # Reading assets on mobile - /// - /// - Android: For loading content from the `assets` folder (which is copied to the Andorid apk) please - /// use the function [`with_asset_loader`] from [`WebViewBuilderExtAndroid`] instead. - /// This function on Android can only be used to serve assets you can embed in the binary or are - /// elsewhere in Android (provided the app has appropriate access), but not from the `assets` - /// folder which lives within the apk. For the cases where this can be used, it works the same as in macOS and Linux. - /// - iOS: To get the path of your assets, you can call [`CFBundle::resources_path`](https://docs.rs/core-foundation/latest/core_foundation/bundle/struct.CFBundle.html#method.resources_path). So url like `wry://assets/index.html` could get the html file in assets directory. - /// - /// [bug]: https://bugs.webkit.org/show_bug.cgi?id=229034 - #[cfg(feature = "protocol")] - pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self - where - F: Fn(Request>) -> HttpResponse> + 'static, - { - self.webview.custom_protocols.push(( - name, - Box::new(move |request, responder| { - let http_response = handler(request); - responder.respond(http_response); - }), - )); - self - } - - /// Same as [`Self::with_custom_protocol`] but with an asynchronous responder. - /// - /// # Examples - /// - /// ```no_run - /// use wry::{ - /// application::{ - /// event_loop::EventLoop, - /// window::WindowBuilder - /// }, - /// webview::WebViewBuilder, - /// }; - /// - /// let event_loop = EventLoop::new(); - /// let window = WindowBuilder::new() - /// .build(&event_loop) - /// .unwrap(); - /// WebViewBuilder::new(window) - /// .unwrap() - /// .with_asynchronous_custom_protocol("wry".into(), |request, responder| { - /// // here you can use a tokio task, thread pool or anything - /// // to do heavy computation to resolve your request - /// // e.g. downloading files, opening the camera... - /// std::thread::spawn(move || { - /// std::thread::sleep(std::time::Duration::from_secs(2)); - /// responder.respond(http::Response::builder().body(Vec::new()).unwrap()); - /// }); - /// }); - /// ``` - #[cfg(feature = "protocol")] - pub fn with_asynchronous_custom_protocol(mut self, name: String, handler: F) -> Self - where - F: Fn(Request>, RequestAsyncResponder) + 'static, - { - self - .webview - .custom_protocols - .push((name, Box::new(handler))); - self - } - - /// Set the IPC handler to receive the message from Javascript on webview to host Rust code. - /// The message sent from webview should call `window.ipc.postMessage("insert_message_here");`. - pub fn with_ipc_handler(mut self, handler: F) -> Self - where - F: Fn(&Window, String) + 'static, - { - self.webview.ipc_handler = Some(Box::new(handler)); - self - } - - /// Set a handler closure to process incoming [`FileDropEvent`] of the webview. - /// - /// # Blocking OS Default Behavior - /// Return `true` in the callback to block the OS' default behavior of handling a file drop. - /// - /// Note, that if you do block this behavior, it won't be possible to drop files on `` forms. - /// Also note, that it's not possible to manually set the value of a `` via JavaScript for security reasons. - #[cfg(feature = "file-drop")] - pub fn with_file_drop_handler(mut self, handler: F) -> Self - where - F: Fn(&Window, FileDropEvent) -> bool + 'static, - { - self.webview.file_drop_handler = Some(Box::new(handler)); - self - } - - /// Load the provided URL with given headers when the builder calling [`WebViewBuilder::build`] to create the - /// [`WebView`]. The provided URL must be valid. - pub fn with_url_and_headers(mut self, url: &str, headers: http::HeaderMap) -> Result { - self.webview.url = Some(url.parse()?); - self.webview.headers = Some(headers); - Ok(self) - } - - /// Load the provided URL when the builder calling [`WebViewBuilder::build`] to create the - /// [`WebView`]. The provided URL must be valid. - pub fn with_url(mut self, url: &str) -> Result { - self.webview.url = Some(Url::parse(url)?); - self.webview.headers = None; - Ok(self) - } - - /// Load the provided HTML string when the builder calling [`WebViewBuilder::build`] to create the - /// [`WebView`]. This will be ignored if `url` is provided. - /// - /// # Warning - /// - /// The Page loaded from html string will have `null` origin. - /// - /// ## PLatform-specific: - /// - /// - **Windows:** the string can not be larger than 2 MB (2 * 1024 * 1024 bytes) in total size - pub fn with_html(mut self, html: impl Into) -> Result { - self.webview.html = Some(html.into()); - Ok(self) - } - - /// Set the web context that can share with multiple [`WebView`]s. - pub fn with_web_context(mut self, web_context: &'a mut WebContext) -> Self { - self.web_context = Some(web_context); - self - } - - /// Set a custom [user-agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) for the WebView. - pub fn with_user_agent(mut self, user_agent: &str) -> Self { - self.webview.user_agent = Some(user_agent.to_string()); - self - } - - /// Enable or disable web inspector which is usually called dev tool. - /// - /// Note this only enables dev tool to the webview. To open it, you can call - /// [`WebView::open_devtools`], or right click the page and open it from the context menu. - /// - /// ## Platform-specific - /// - /// - macOS: This will call private functions on **macOS**. It's still enabled if set in **debug** build on mac, - /// but requires `devtools` feature flag to actually enable it in **release** build. - /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. - /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. - pub fn with_devtools(mut self, devtools: bool) -> Self { - self.webview.devtools = devtools; - self - } - - /// Whether page zooming by hotkeys or gestures is enabled - /// - /// ## Platform-specific - /// - /// **macOS / Linux / Android / iOS**: Unsupported - pub fn with_hotkeys_zoom(mut self, zoom: bool) -> Self { - self.webview.zoom_hotkeys_enabled = zoom; - self - } - - /// Set a navigation handler to decide if incoming url is allowed to navigate. - /// - /// The closure takes a `String` parameter as url and return `bool` to determine the url. True is - /// allowed to navigate and false is not. - pub fn with_navigation_handler(mut self, callback: impl Fn(String) -> bool + 'static) -> Self { - self.webview.navigation_handler = Some(Box::new(callback)); - self - } - - /// Set a download started handler to manage incoming downloads. - /// - /// The closure takes two parameters - the first is a `String` representing the url being downloaded from and and the - /// second is a mutable `PathBuf` reference that (possibly) represents where the file will be downloaded to. The latter - /// parameter can be used to set the download location by assigning a new path to it - the assigned path _must_ be - /// absolute. The closure returns a `bool` to allow or deny the download. - pub fn with_download_started_handler( - mut self, - started_handler: impl FnMut(String, &mut PathBuf) -> bool + 'static, - ) -> Self { - self.webview.download_started_handler = Some(Box::new(started_handler)); - self - } - - /// Sets a download completion handler to manage downloads that have finished. - /// - /// The closure is fired when the download completes, whether it was successful or not. - /// The closure takes a `String` representing the URL of the original download request, an `Option` - /// potentially representing the filesystem path the file was downloaded to, and a `bool` indicating if the download - /// succeeded. A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download - /// did not succeed, and may instead indicate some other failure - always check the third parameter if you need to - /// know if the download succeeded. - /// - /// ## Platform-specific: - /// - /// - **macOS**: The second parameter indicating the path the file was saved to is always empty, due to API - /// limitations. - pub fn with_download_completed_handler( - mut self, - download_completed_handler: impl Fn(String, Option, bool) + 'static, - ) -> Self { - self.webview.download_completed_handler = Some(Rc::new(download_completed_handler)); - self - } - - /// Enables clipboard access for the page rendered on **Linux** and **Windows**. - /// - /// macOS doesn't provide such method and is always enabled by default. But you still need to add menu - /// item accelerators to use shortcuts. - pub fn with_clipboard(mut self, clipboard: bool) -> Self { - self.webview.clipboard = clipboard; - self - } - - /// Set a new window request handler to decide if incoming url is allowed to be opened. - /// - /// The closure takes a `String` parameter as url and return `bool` to determine if the url can be - /// opened in a new window. Returning true will open the url in a new window, whilst returning false - /// will neither open a new window nor allow any navigation. - pub fn with_new_window_req_handler( - mut self, - callback: impl Fn(String) -> bool + 'static, - ) -> Self { - self.webview.new_window_req_handler = Some(Box::new(callback)); - self - } - - /// Sets whether clicking an inactive window also clicks through to the webview. Default is `false`. - /// - /// ## Platform-specific - /// - /// This configuration only impacts macOS. - pub fn with_accept_first_mouse(mut self, accept_first_mouse: bool) -> Self { - self.webview.accept_first_mouse = accept_first_mouse; - self - } - - /// Set a handler closure to process the change of the webview's document title. - pub fn with_document_title_changed_handler( - mut self, - callback: impl Fn(&Window, String) + 'static, - ) -> Self { - self.webview.document_title_changed_handler = Some(Box::new(callback)); - self - } - - /// Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is - /// enabled. - /// - /// ## Platform-specific: - /// - /// - **Android:** Unsupported yet. - pub fn with_incognito(mut self, incognito: bool) -> Self { - self.webview.incognito = incognito; - self - } - - /// Set a handler to process page loading events. - /// - /// The handler will be called when the webview begins the indicated loading event. - pub fn with_on_page_load_handler( - mut self, - handler: impl Fn(PageLoadEvent, String) + 'static, - ) -> Self { - self.webview.on_page_load_handler = Some(Box::new(handler)); - self - } - - /// Set a proxy configuration for the webview. - /// - /// - **macOS**: Requires macOS 14.0+ and the `mac-proxy` feature flag to be enabled. Supports HTTP CONNECT and SOCKSv5 proxies. - /// - **Windows / Linux**: Supports HTTP CONNECT and SOCKSv5 proxies. - /// - **Android / iOS:** Not supported. - pub fn with_proxy_config(mut self, configuration: ProxyConfig) -> Self { - self.webview.proxy_config = Some(configuration); - self - } - - /// Set whether the webview should be focused when created. - /// - /// ## Platform-specific: - /// - /// - **macOS / Android / iOS:** Unsupported. - pub fn with_focused(mut self, focused: bool) -> Self { - self.webview.focused = focused; - self - } - - /// Consume the builder and create the [`WebView`]. - /// - /// Platform-specific behavior: - /// - /// - **Unix:** This method must be called in a gtk thread. Usually this means it should be - /// called in the same thread with the [`EventLoop`] you create. - /// - /// [`EventLoop`]: crate::application::event_loop::EventLoop - pub fn build(self) -> Result { - let window = Rc::new(self.window); - let webview = InnerWebView::new( - window.clone(), - self.webview, - self.platform_specific, - self.web_context, - )?; - Ok(WebView { window, webview }) - } -} - -#[cfg(windows)] -pub trait WebViewBuilderExtWindows { - /// Pass additional args to Webview2 upon creating the webview. - /// - /// ## Warning - /// - /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` - /// `--autoplay-policy=no-user-gesture-required` if autoplay is enabled - /// and `--proxy-server=://:` if a proxy is set. - /// so if you use this method, you have to add these arguments yourself if you want to keep the same behavior. - fn with_additional_browser_args>(self, additional_args: S) -> Self; - - /// Determines whether browser-specific accelerator keys are enabled. When this setting is set to - /// `false`, it disables all accelerator keys that access features specific to a web browser. - /// The default value is `true`. See the following link to know more details. - /// - /// https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings#arebrowseracceleratorkeysenabled - fn with_browser_accelerator_keys(self, enabled: bool) -> Self; - - /// Specifies the theme of webview2. This affects things like `prefers-color-scheme`. - /// - /// Defaults to [`Theme::Auto`] which will follow the OS defaults. - fn with_theme(self, theme: Theme) -> Self; - - /// Determines whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost`. - /// - /// Using a `http` scheme will allow mixed content when trying to fetch `http` endpoints - /// and is therefore less secure but will match the behavior of the `://localhost` protocols used on macOS and Linux. - /// - /// The default value is `false`. - fn with_https_scheme(self, enabled: bool) -> Self; -} - -#[cfg(windows)] -impl WebViewBuilderExtWindows for WebViewBuilder<'_> { - fn with_additional_browser_args>(mut self, additional_args: S) -> Self { - self.platform_specific.additional_browser_args = Some(additional_args.into()); - self - } - - fn with_browser_accelerator_keys(mut self, enabled: bool) -> Self { - self.platform_specific.browser_accelerator_keys = enabled; - self - } - - fn with_theme(mut self, theme: Theme) -> Self { - self.platform_specific.theme = Some(theme); - self - } - - fn with_https_scheme(mut self, enabled: bool) -> Self { - self.platform_specific.https_scheme = enabled; - self - } -} - -#[cfg(target_os = "android")] -pub trait WebViewBuilderExtAndroid { - fn on_webview_created< - F: Fn( - prelude::Context<'_, '_>, - ) -> std::result::Result<(), tao::platform::android::ndk_glue::jni::errors::Error> - + Send - + 'static, - >( - self, - f: F, - ) -> Self; - - /// Use [WebviewAssetLoader](https://developer.android.com/reference/kotlin/androidx/webkit/WebViewAssetLoader) - /// to load assets from Android's `asset` folder when using `with_url` as `://assets/` (e.g.: - /// `wry://assets/index.html`). Note that this registers a custom protocol with the provided - /// String, similar to [`with_custom_protocol`], but also sets the WebViewAssetLoader with the - /// necessary domain (which is fixed as `.assets`). This cannot be used in conjunction - /// to `with_custom_protocol` for Android, as it changes the way in which requests are handled. - #[cfg(feature = "protocol")] - fn with_asset_loader(self, protocol: String) -> Self; - - /// Determines whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost`. - /// - /// Using a `http` scheme will allow mixed content when trying to fetch `http` endpoints - /// and is therefore less secure but will match the behavior of the `://localhost` protocols used on macOS and Linux. - /// - /// The default value is `false`. - fn with_https_scheme(self, enabled: bool) -> Self; -} - -#[cfg(target_os = "android")] -impl WebViewBuilderExtAndroid for WebViewBuilder<'_> { - fn on_webview_created< - F: Fn( - prelude::Context<'_, '_>, - ) -> std::result::Result<(), tao::platform::android::ndk_glue::jni::errors::Error> - + Send - + 'static, - >( - mut self, - f: F, - ) -> Self { - self.platform_specific.on_webview_created = Some(Box::new(f)); - self - } - - #[cfg(feature = "protocol")] - fn with_asset_loader(mut self, protocol: String) -> Self { - // register custom protocol with empty Response return, - // this is necessary due to the need of fixing a domain - // in WebViewAssetLoader. - self.webview.custom_protocols.push(( - protocol.clone(), - Box::new(|_, api| { - api.respond(HttpResponse::builder().body(Vec::new()).unwrap()); - }), - )); - self.platform_specific.with_asset_loader = true; - self.platform_specific.asset_loader_domain = Some(format!("{}.assets", protocol)); - self - } - - fn with_https_scheme(mut self, enabled: bool) -> Self { - self.platform_specific.https_scheme = enabled; - self - } -} - -/// The fundamental type to present a [`WebView`]. -/// -/// [`WebViewBuilder`] / [`WebView`] are the basic building blocks to construct WebView contents and -/// scripts for those who prefer to control fine grained window creation and event handling. -/// [`WebView`] presents the actual WebView window and let you still able to perform actions -/// during event handling to it. [`WebView`] also contains the associate [`Window`] with it. -pub struct WebView { - window: Rc, - webview: InnerWebView, -} - -// Signal the Window to drop on Linux and Windows. On mac, we need to handle several unsafe code -// blocks and raw pointer properly. -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -impl Drop for WebView { - fn drop(&mut self) { - unsafe { - use crate::application::platform::unix::WindowExtUnix; - use gtk::prelude::WidgetExtManual; - self.window().gtk_window().destroy(); - } - } -} - -#[cfg(target_os = "windows")] -impl Drop for WebView { - fn drop(&mut self) { - unsafe { - let _ = DestroyWindow(HWND(self.window.hwnd() as _)); - } - } -} - -impl WebView { - /// Create a [`WebView`] from provided [`Window`]. Note that calling this directly loses - /// abilities to initialize scripts, add ipc handler, and many more before starting WebView. To - /// benefit from above features, create a [`WebViewBuilder`] instead. - /// - /// Platform-specific behavior: - /// - /// - **Unix:** This method must be called in a gtk thread. Usually this means it should be - /// called in the same thread with the [`EventLoop`] you create. - /// - /// [`EventLoop`]: crate::application::event_loop::EventLoop - pub fn new(window: Window) -> Result { - WebViewBuilder::new(window)?.build() - } - - /// Get the [`Window`] associate with the [`WebView`]. This can let you perform window related - /// actions. - pub fn window(&self) -> &Window { - &self.window - } - - /// Get the current url of the webview - pub fn url(&self) -> Url { - self.webview.url() - } - - /// Evaluate and run javascript code. Must be called on the same thread who created the - /// [`WebView`]. Use [`EventLoopProxy`] and a custom event to send scripts from other threads. - /// - /// [`EventLoopProxy`]: crate::application::event_loop::EventLoopProxy - /// - pub fn evaluate_script(&self, js: &str) -> Result<()> { - self - .webview - .eval(js, None::>) - } - - /// Evaluate and run javascript code with callback function. The evaluation result will be - /// serialized into a JSON string and passed to the callback function. Must be called on the - /// same thread who created the [`WebView`]. Use [`EventLoopProxy`] and a custom event to - /// send scripts from other threads. - /// - /// [`EventLoopProxy`]: crate::application::event_loop::EventLoopProxy - /// - /// Exception is ignored because of the limitation on windows. You can catch it yourself and return as string as a workaround. - /// - /// - ** Android:** Not implemented yet. - pub fn evaluate_script_with_callback( - &self, - js: &str, - callback: impl Fn(String) + Send + 'static, - ) -> Result<()> { - self.webview.eval(js, Some(callback)) - } - - /// Launch print modal for the webview content. - pub fn print(&self) -> Result<()> { - self.webview.print(); - Ok(()) - } - - /// Open the web inspector which is usually called dev tool. - /// - /// ## Platform-specific - /// - /// - **Android / iOS:** Not supported. - #[cfg(any(debug_assertions, feature = "devtools"))] - pub fn open_devtools(&self) { - self.webview.open_devtools(); - } - - /// Close the web inspector which is usually called dev tool. - /// - /// ## Platform-specific - /// - /// - **Windows / Android / iOS:** Not supported. - #[cfg(any(debug_assertions, feature = "devtools"))] - pub fn close_devtools(&self) { - self.webview.close_devtools(); - } - - /// Gets the devtool window's current visibility state. - /// - /// ## Platform-specific - /// - /// - **Windows / Android / iOS:** Not supported. - #[cfg(any(debug_assertions, feature = "devtools"))] - pub fn is_devtools_open(&self) -> bool { - self.webview.is_devtools_open() - } - - /// Gets the physical size of the webview’s client area. This is - /// a drop-in replacement for [`Window::inner_size`] because on some platforms - /// (currently, only macOS), it will return an incorrect size. - /// - /// ```no_run - /// use wry::{ - /// application::{ - /// event_loop::EventLoop, - /// window::WindowBuilder - /// }, - /// webview::WebViewBuilder, - /// }; - /// let event_loop = EventLoop::new(); - /// let window = WindowBuilder::new().build(&event_loop).unwrap(); - /// let webview = WebViewBuilder::new(window) - /// .unwrap() - /// .build() - /// .unwrap(); - /// - /// // This returns incorrect window size on macOS. - /// println!("{:?}", webview.window().inner_size()); - /// // Instead, this always returns the correct window size. - /// println!("{:?}", webview.inner_size()); - /// ``` - pub fn inner_size(&self) -> PhysicalSize { - #[cfg(target_os = "macos")] - { - let scale_factor = self.window.scale_factor(); - self.webview.inner_size(scale_factor) - } - #[cfg(not(target_os = "macos"))] - self.window.inner_size() - } - - /// Set the webview zoom level - /// - /// ## Platform-specific: - /// - /// - **Android**: Not supported. - /// - **macOS**: available on macOS 11+ only. - /// - **iOS**: available on iOS 14+ only. - pub fn zoom(&self, scale_factor: f64) { - self.webview.zoom(scale_factor); - } - - /// Specify the webview background color. - /// - /// The color uses the RGBA format. - /// - /// ## Platfrom-specific: - /// - /// - **macOS / iOS**: Not implemented. - /// - **Windows**: - /// - On Windows 7, transparency is not supported and the alpha value will be ignored. - /// - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` - pub fn set_background_color(&self, background_color: RGBA) -> Result<()> { - self.webview.set_background_color(background_color) - } - - /// Navigate to the specified url - pub fn load_url(&self, url: &str) { - self.webview.load_url(url) - } - - /// Navigate to the specified url using the specified headers - pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) { - self.webview.load_url_with_headers(url, headers) - } - - /// Clear all browsing data - pub fn clear_all_browsing_data(&self) -> Result<()> { - self.webview.clear_all_browsing_data() - } -} - -/// An event enumeration sent to [`FileDropHandler`]. -#[non_exhaustive] -#[derive(Debug, Serialize, Clone)] -pub enum FileDropEvent { - /// The file(s) have been dragged onto the window, but have not been dropped yet. - Hovered { - paths: Vec, - /// The position of the mouse cursor. - position: PhysicalPosition, - }, - /// The file(s) have been dropped onto the window. - Dropped { - paths: Vec, - /// The position of the mouse cursor. - position: PhysicalPosition, - }, - /// The file drop was aborted. - Cancelled, -} - -/// Get Webview/Webkit version on current platform. -pub fn webview_version() -> Result { - platform_webview_version() -} - -/// The [memory usage target level][1]. There are two levels 'Low' and 'Normal' and the default -/// level is 'Normal'. When the application is going inactive, setting the level to 'Low' can -/// significantly reduce the application's memory consumption. -/// -/// [1]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2memoryusagetargetlevel -#[cfg(target_os = "windows")] -#[non_exhaustive] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum MemoryUsageLevel { - /// The 'Normal' memory usage. Applications should set this level when they are becoming active. - #[default] - Normal, - /// The 'Low' memory usage. Applications can reduce memory comsumption by setting this level when - /// they are becoming inactive. - Low, -} - -/// Additional methods on `WebView` that are specific to Windows. -#[cfg(target_os = "windows")] -pub trait WebviewExtWindows { - /// Returns WebView2 Controller - fn controller(&self) -> ICoreWebView2Controller; - - /// Changes the webview2 theme. - fn set_theme(&self, theme: Theme); - - /// Sets the [memory usage target level][1]. - /// - /// When to best use this mode depends on the app in question. Most commonly it's called when - /// the app's visiblity state changes. - /// - /// Please read the [guide for WebView2][2] for more details. - /// - /// This method uses a WebView2 API added in Runtime version 114.0.1823.32. When it is used in - /// an older Runtime version, it does nothing. - /// - /// [1]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2memoryusagetargetlevel - /// [2]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.memoryusagetargetlevel?view=webview2-dotnet-1.0.2088.41#remarks - fn set_memory_usage_level(&self, level: MemoryUsageLevel); -} - -#[cfg(target_os = "windows")] -impl WebviewExtWindows for WebView { - fn controller(&self) -> ICoreWebView2Controller { - self.webview.controller.clone() - } - - fn set_theme(&self, theme: Theme) { - self.webview.set_theme(theme) - } - - fn set_memory_usage_level(&self, level: MemoryUsageLevel) { - self.webview.set_memory_usage_level(level); - } -} - -/// Additional methods on `WebView` that are specific to Linux. -#[cfg(target_os = "linux")] -pub trait WebviewExtUnix { - /// Returns Webkit2gtk Webview handle - fn webview(&self) -> Rc; -} - -#[cfg(target_os = "linux")] -impl WebviewExtUnix for WebView { - fn webview(&self) -> Rc { - self.webview.webview.clone() - } -} - -/// Additional methods on `WebView` that are specific to macOS. -#[cfg(target_os = "macos")] -pub trait WebviewExtMacOS { - /// Returns WKWebView handle - fn webview(&self) -> cocoa::base::id; - /// Returns WKWebView manager [(userContentController)](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller) handle - fn manager(&self) -> cocoa::base::id; - /// Returns NSWindow associated with the WKWebView webview - fn ns_window(&self) -> cocoa::base::id; -} - -#[cfg(target_os = "macos")] -impl WebviewExtMacOS for WebView { - fn webview(&self) -> cocoa::base::id { - self.webview.webview - } - - fn manager(&self) -> cocoa::base::id { - self.webview.manager - } - - fn ns_window(&self) -> cocoa::base::id { - self.webview.ns_window - } -} - -/// Additional methods on `WebView` that are specific to iOS. -#[cfg(target_os = "ios")] -pub trait WebviewExtIOS { - /// Returns WKWebView handle - fn webview(&self) -> cocoa::base::id; - /// Returns WKWebView manager [(userContentController)](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller) handle - fn manager(&self) -> cocoa::base::id; -} - -#[cfg(target_os = "ios")] -impl WebviewExtIOS for WebView { - fn webview(&self) -> cocoa::base::id { - self.webview.webview - } - - fn manager(&self) -> cocoa::base::id { - self.webview.manager - } -} - -#[cfg(target_os = "android")] -/// Additional methods on `WebView` that are specific to Android -pub trait WebviewExtAndroid { - fn handle(&self) -> JniHandle; -} - -#[cfg(target_os = "android")] -impl WebviewExtAndroid for WebView { - fn handle(&self) -> JniHandle { - JniHandle - } -} - -#[derive(Debug, Clone, Copy)] -pub enum Theme { - Dark, - Light, - Auto, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn should_get_webview_version() { - if let Err(error) = webview_version() { - panic!("{}", error); - } - } -} diff --git a/src/webview/webkitgtk/undecorated_resizing.rs b/src/webview/webkitgtk/undecorated_resizing.rs deleted file mode 100644 index f4fb5f8bf..000000000 --- a/src/webview/webkitgtk/undecorated_resizing.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::application::platform::unix::*; -use gtk::{ - gdk::{Cursor, WindowEdge}, - prelude::*, -}; -use webkit2gtk::WebView; - -pub fn setup(webview: &WebView) { - webview.connect_motion_notify_event(|webview, event| { - // This one should be GtkWindow - if let Some(widget) = webview.parent() { - // This one should be GtkWindow - if let Some(window) = widget.parent() { - // Safe to unwrap unless this is not from tao - let window: gtk::Window = window.downcast().unwrap(); - if !window.is_decorated() && window.is_resizable() { - if let Some(window) = window.window() { - let (cx, cy) = event.root(); - let edge = hit_test(&window, cx, cy); - // FIXME: calling `window.begin_resize_drag` seems to revert the cursor back to normal style - window.set_cursor( - Cursor::from_name( - &window.display(), - match edge { - WindowEdge::North => "n-resize", - WindowEdge::South => "s-resize", - WindowEdge::East => "e-resize", - WindowEdge::West => "w-resize", - WindowEdge::NorthWest => "nw-resize", - WindowEdge::NorthEast => "ne-resize", - WindowEdge::SouthEast => "se-resize", - WindowEdge::SouthWest => "sw-resize", - _ => "default", - }, - ) - .as_ref(), - ); - } - } - } - } - gtk::glib::Propagation::Proceed - }); - webview.connect_button_press_event(move |webview, event| { - if event.button() == 1 { - let (cx, cy) = event.root(); - // This one should be GtkBox - if let Some(widget) = webview.parent() { - // This one should be GtkWindow - if let Some(window) = widget.parent() { - // Safe to unwrap unless this is not from tao - let window: gtk::Window = window.downcast().unwrap(); - if !window.is_decorated() && window.is_resizable() { - if let Some(window) = window.window() { - // Safe to unwrap since it's a valid GtkWindow - let result = hit_test(&window, cx, cy); - - // we ignore the `__Unknown` variant so the webview receives the click correctly if it is not on the edges. - match result { - WindowEdge::__Unknown(_) => (), - _ => window.begin_resize_drag(result, 1, cx as i32, cy as i32, event.time()), - } - } - } - } - } - } - gtk::glib::Propagation::Proceed - }); - webview.connect_touch_event(|webview, event| { - // This one should be GtkBox - if let Some(widget) = webview.parent() { - // This one should be GtkWindow - if let Some(window) = widget.parent() { - // Safe to unwrap unless this is not from tao - let window: gtk::Window = window.downcast().unwrap(); - if !window.is_decorated() && window.is_resizable() && !window.is_maximized() { - if let Some(window) = window.window() { - if let Some((cx, cy)) = event.root_coords() { - if let Some(device) = event.device() { - let result = hit_test(&window, cx, cy); - - // we ignore the `__Unknown` variant so the window receives the click correctly if it is not on the edges. - match result { - WindowEdge::__Unknown(_) => (), - _ => window.begin_resize_drag_for_device( - result, - &device, - 0, - cx as i32, - cy as i32, - event.time(), - ), - } - } - } - } - } - } - } - gtk::glib::Propagation::Proceed - }); -} diff --git a/src/webview/webview2/resize.rs b/src/webview/webview2/resize.rs deleted file mode 100644 index 65ddcac86..000000000 --- a/src/webview/webview2/resize.rs +++ /dev/null @@ -1,155 +0,0 @@ -#![allow(non_snake_case)] - -use once_cell::sync::Lazy; -use windows::{ - core::HRESULT, - Win32::{ - Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM}, - Graphics::Gdi::{ - GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST, - }, - UI::{ - HiDpi::{MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE}, - Input::KeyboardAndMouse::ReleaseCapture, - WindowsAndMessaging::{ - GetWindowRect, IsProcessDPIAware, PostMessageW, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, - HTCLIENT, HTLEFT, HTNOWHERE, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, - }, - }, - }, -}; - -use super::get_function; - -#[inline] -pub fn MAKELPARAM(x: i16, y: i16) -> LPARAM { - LPARAM(((x as u16 as u32) | ((y as u16 as u32) << 16)) as usize as _) -} - -#[inline] -pub fn begin_resize_drag( - hwnd: isize, - edge: isize, - button: u32, - x: i32, - y: i32, -) -> windows::core::Result<()> { - unsafe { - let w_param = WPARAM(edge as _); - let l_param = MAKELPARAM(x as i16, y as i16); - - ReleaseCapture()?; - PostMessageW(HWND(hwnd), button, w_param, l_param) - } -} - -type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; -type GetDpiForMonitor = unsafe extern "system" fn( - hmonitor: HMONITOR, - dpi_type: MONITOR_DPI_TYPE, - dpi_x: *mut u32, - dpi_y: *mut u32, -) -> HRESULT; - -static GET_DPI_FOR_WINDOW: Lazy> = - Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); -static GET_DPI_FOR_MONITOR: Lazy> = - Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); - -const BASE_DPI: u32 = 96; -fn dpi_to_scale_factor(dpi: u32) -> f64 { - dpi as f64 / BASE_DPI as f64 -} - -unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { - let hdc = GetDC(hwnd); - if hdc.is_invalid() { - panic!("[tao] `GetDC` returned null!"); - } - if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { - // We are on Windows 10 Anniversary Update (1607) or later. - match GetDpiForWindow(hwnd) { - 0 => BASE_DPI, // 0 is returned if hwnd is invalid - dpi => dpi as u32, - } - } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { - // We are on Windows 8.1 or later. - let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - if monitor.is_invalid() { - return BASE_DPI; - } - - let mut dpi_x = 0; - let mut dpi_y = 0; - if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y).is_ok() { - dpi_x as u32 - } else { - BASE_DPI - } - } else { - // We are on Vista or later. - if IsProcessDPIAware().as_bool() { - // If the process is DPI aware, then scaling must be handled by the application using - // this DPI value. - GetDeviceCaps(hdc, LOGPIXELSX) as u32 - } else { - // If the process is DPI unaware, then scaling is performed by the OS; we thus return - // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the - // application and the WM. - BASE_DPI - } - } -} - -const BORDERLESS_RESIZE_INSET: i32 = 5; - -pub fn hit_test(hwnd: isize, cx: i32, cy: i32) -> LRESULT { - let hwnd = HWND(hwnd); - let mut window_rect = RECT::default(); - unsafe { - if GetWindowRect(hwnd, &mut window_rect).is_ok() { - const CLIENT: isize = 0b0000; - const LEFT: isize = 0b0001; - const RIGHT: isize = 0b0010; - const TOP: isize = 0b0100; - const BOTTOM: isize = 0b1000; - const TOPLEFT: isize = TOP | LEFT; - const TOPRIGHT: isize = TOP | RIGHT; - const BOTTOMLEFT: isize = BOTTOM | LEFT; - const BOTTOMRIGHT: isize = BOTTOM | RIGHT; - - let RECT { - left, - right, - bottom, - top, - } = window_rect; - - let dpi = hwnd_dpi(hwnd); - let scale_factor = dpi_to_scale_factor(dpi); - let inset = (BORDERLESS_RESIZE_INSET as f64 * scale_factor) as i32; - - #[rustfmt::skip] - let result = - (LEFT * (if cx < (left + inset) { 1 } else { 0 })) - | (RIGHT * (if cx >= (right - inset) { 1 } else { 0 })) - | (TOP * (if cy < (top + inset) { 1 } else { 0 })) - | (BOTTOM * (if cy >= (bottom - inset) { 1 } else { 0 })); - - LRESULT(match result { - CLIENT => HTCLIENT, - LEFT => HTLEFT, - RIGHT => HTRIGHT, - TOP => HTTOP, - BOTTOM => HTBOTTOM, - TOPLEFT => HTTOPLEFT, - TOPRIGHT => HTTOPRIGHT, - BOTTOMLEFT => HTBOTTOMLEFT, - BOTTOMRIGHT => HTBOTTOMRIGHT, - _ => HTNOWHERE, - } as _) - } else { - LRESULT(HTNOWHERE as _) - } - } -} diff --git a/src/webview/webview2/file_drop.rs b/src/webview2/file_drop.rs similarity index 63% rename from src/webview/webview2/file_drop.rs rename to src/webview2/file_drop.rs index 967f4b942..667dc58ca 100644 --- a/src/webview/webview2/file_drop.rs +++ b/src/webview2/file_drop.rs @@ -2,12 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::webview::FileDropEvent; - // A silly implementation of file drop handling for Windows! -// This can be pretty much entirely replaced when WebView2 SDK 1.0.721-prerelease becomes stable. -// https://docs.microsoft.com/en-us/microsoft-edge/webview2/releasenotes#10721-prerelease -// https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2experimentalcompositioncontroller3?view=webview2-1.0.721-prerelease&preserve-view=true + +use crate::FileDropEvent; use std::{ cell::UnsafeCell, @@ -37,90 +34,57 @@ use windows::Win32::{ use windows_implement::implement; -use crate::application::{ - dpi::PhysicalPosition, platform::windows::WindowExtWindows, window::Window, -}; - +#[derive(Default)] pub(crate) struct FileDropController { drop_targets: Vec, } impl FileDropController { - pub(crate) fn new() -> Self { - FileDropController { - drop_targets: Vec::new(), - } - } + pub(crate) fn new(hwnd: HWND, handler: Box bool>) -> Self { + let mut controller = FileDropController::default(); - pub(crate) fn listen( - &mut self, - hwnd: HWND, - window: Rc, - handler: Box bool>, - ) { - let listener = Rc::new(handler); + let handler = Rc::new(handler); + let mut callback = |hwnd| controller.inject_in_hwnd(hwnd, handler.clone()); // Enumerate child windows to find the WebView2 "window" and override! - enumerate_child_windows(hwnd, |hwnd| { - self.inject(hwnd, window.clone(), listener.clone()) - }); - } - - fn inject( - &mut self, - hwnd: HWND, - window: Rc, - listener: Rc bool>, - ) -> bool { - // Safety: WinAPI calls are unsafe - unsafe { - let file_drop_handler: IDropTarget = FileDropHandler::new(window, listener).into(); - - if RevokeDragDrop(hwnd) != Err(DRAGDROP_E_INVALIDHWND.into()) - && RegisterDragDrop(hwnd, &file_drop_handler).is_ok() - { - // Not a great solution. But there is no reliable way to get the window handle of the webview, for whatever reason... - self.drop_targets.push(file_drop_handler); + { + let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback; + let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) }; + let lparam = LPARAM(closure_pointer_pointer as _); + unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL { + let closure = &mut *(lparam.0 as *mut c_void as *mut &mut dyn FnMut(HWND) -> bool); + closure(hwnd).into() } + unsafe { EnumChildWindows(hwnd, Some(enumerate_callback), lparam) }; } - true + controller } -} -// https://gist.github.com/application-developer-DA/5a460d9ca02948f1d2bfa53100c941da -// Safety: WinAPI calls are unsafe - -fn enumerate_child_windows(hwnd: HWND, mut callback: F) -where - F: FnMut(HWND) -> bool, -{ - let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback; - let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) }; - let lparam = LPARAM(closure_pointer_pointer as _); - unsafe { EnumChildWindows(hwnd, Some(enumerate_callback), lparam) }; -} + fn inject_in_hwnd(&mut self, hwnd: HWND, handler: Rc bool>) -> bool { + let file_drop_handler: IDropTarget = FileDropHandler::new(hwnd, handler).into(); + if unsafe { RevokeDragDrop(hwnd) } != Err(DRAGDROP_E_INVALIDHWND.into()) + && unsafe { RegisterDragDrop(hwnd, &file_drop_handler) }.is_ok() + { + self.drop_targets.push(file_drop_handler); + } -unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL { - let closure = &mut *(lparam.0 as *mut c_void as *mut &mut dyn FnMut(HWND) -> bool); - closure(hwnd).into() + true + } } #[implement(IDropTarget)] pub struct FileDropHandler { - window: Rc, - listener: Rc bool>, + hwnd: HWND, + listener: Rc bool>, cursor_effect: UnsafeCell, hovered_is_valid: UnsafeCell, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */ } impl FileDropHandler { - pub fn new( - window: Rc, - listener: Rc bool>, - ) -> FileDropHandler { + pub fn new(hwnd: HWND, listener: Rc bool>) -> FileDropHandler { Self { - window, + hwnd, listener, cursor_effect: DROPEFFECT_NONE.into(), hovered_is_valid: false.into(), @@ -208,16 +172,13 @@ impl IDropTarget_Impl for FileDropHandler { *self.cursor_effect.get() = cursor_effect; let mut pt = POINT { x: pt.x, y: pt.y }; - ScreenToClient(HWND(self.window.hwnd() as _), &mut pt); + ScreenToClient(self.hwnd, &mut pt); } - (self.listener)( - &self.window, - FileDropEvent::Hovered { - paths, - position: PhysicalPosition::new(pt.x as _, pt.y as _), - }, - ); + (self.listener)(FileDropEvent::Hovered { + paths, + position: (pt.x as _, pt.y as _), + }); Ok(()) } @@ -234,7 +195,7 @@ impl IDropTarget_Impl for FileDropHandler { fn DragLeave(&self) -> windows::core::Result<()> { if unsafe { *self.hovered_is_valid.get() } { - (self.listener)(&self.window, FileDropEvent::Cancelled); + (self.listener)(FileDropEvent::Cancelled); } Ok(()) } @@ -254,16 +215,13 @@ impl IDropTarget_Impl for FileDropHandler { } let mut pt = POINT { x: pt.x, y: pt.y }; - ScreenToClient(HWND(self.window.hwnd() as _), &mut pt); + ScreenToClient(self.hwnd, &mut pt); } - (self.listener)( - &self.window, - FileDropEvent::Dropped { - paths, - position: PhysicalPosition::new(pt.x as _, pt.y as _), - }, - ); + (self.listener)(FileDropEvent::Dropped { + paths, + position: (pt.x as _, pt.y as _), + }); Ok(()) } diff --git a/src/webview/webview2/mod.rs b/src/webview2/mod.rs similarity index 84% rename from src/webview/webview2/mod.rs rename to src/webview2/mod.rs index 0d7455dd0..8524b063f 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -3,57 +3,48 @@ // SPDX-License-Identifier: MIT mod file_drop; -mod resize; - -use crate::{ - webview::{ - proxy::ProxyConfig, MemoryUsageLevel, PageLoadEvent, RequestAsyncResponder, WebContext, - WebViewAttributes, RGBA, - }, - Error, Result, -}; - -use file_drop::FileDropController; -use url::Url; use std::{ - borrow::Cow, - collections::HashSet, - fmt::Write, - iter::once, - os::windows::prelude::OsStrExt, - path::PathBuf, - rc::Rc, - sync::{mpsc, Arc}, + borrow::Cow, collections::HashSet, fmt::Write, iter::once, os::windows::prelude::OsStrExt, + path::PathBuf, rc::Rc, sync::mpsc, }; -use once_cell::{sync::Lazy, unsync::OnceCell}; - +use http::{Request, Response as HttpResponse, StatusCode}; +use once_cell::sync::Lazy; +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; +use url::Url; +use webview2_com::{Microsoft::Web::WebView2::Win32::*, *}; use windows::{ core::{s, ComInterface, PCSTR, PCWSTR, PWSTR}, Win32::{ Foundation::*, Globalization::{self, MAX_LOCALE_NAME}, - Graphics::Gdi::{RedrawWindow, HRGN, RDW_INTERNALPAINT}, + Graphics::Gdi::{RedrawWindow, HBRUSH, HRGN, RDW_INTERNALPAINT}, System::{ - Com::IStream, - LibraryLoader::{GetProcAddress, LoadLibraryW}, + Com::{CoInitializeEx, IStream, COINIT_APARTMENTTHREADED}, + LibraryLoader::{GetModuleHandleW, GetProcAddress, LoadLibraryW}, SystemInformation::OSVERSIONINFOW, WinRT::EventRegistrationToken, }, UI::{ Shell::{DefSubclassProc, SHCreateMemStream, SetWindowSubclass}, - WindowsAndMessaging::{self as win32wm, PostMessageW, RegisterWindowMessageA}, + WindowsAndMessaging::{ + self as win32wm, CreateWindowExW, DefWindowProcW, DestroyWindow, PostMessageW, + RegisterClassExW, RegisterWindowMessageA, SetWindowPos, ShowWindow, CS_HREDRAW, CS_VREDRAW, + CW_USEDEFAULT, HCURSOR, HICON, HMENU, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOMOVE, + SWP_NOSIZE, SWP_NOZORDER, SW_HIDE, SW_SHOW, WINDOW_EX_STYLE, WNDCLASSEXW, WS_CHILD, + WS_CLIPCHILDREN, WS_VISIBLE, + }, }, }, }; -use webview2_com::{Microsoft::Web::WebView2::Win32::*, *}; - -use crate::application::{platform::windows::WindowExtWindows, window::Window}; -use http::{Request, Response as HttpResponse, StatusCode}; - +use self::file_drop::FileDropController; use super::Theme; +use crate::{ + proxy::ProxyConfig, Error, MemoryUsageLevel, PageLoadEvent, RequestAsyncResponder, Result, + WebContext, WebViewAttributes, RGBA, +}; impl From for Error { fn from(err: webview2_com::Error) -> Self { @@ -62,39 +53,137 @@ impl From for Error { } pub(crate) struct InnerWebView { + hwnd: HWND, + is_child: bool, pub controller: ICoreWebView2Controller, webview: ICoreWebView2, env: ICoreWebView2Environment, // Store FileDropController in here to make sure it gets dropped when // the webview gets dropped, otherwise we'll have a memory leak #[allow(dead_code)] - file_drop_controller: Rc>, + file_drop_controller: Option, +} + +impl Drop for InnerWebView { + fn drop(&mut self) { + let _ = unsafe { self.controller.Close() }; + if self.is_child { + let _ = unsafe { DestroyWindow(self.hwnd) }; + } + } } impl InnerWebView { pub fn new( - window: Rc, + window: &impl HasRawWindowHandle, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + web_context: Option<&mut WebContext>, + ) -> Result { + let window = match window.raw_window_handle() { + RawWindowHandle::Win32(window) => window.hwnd as _, + _ => return Err(Error::UnsupportedWindowHandle), + }; + Self::new_hwnd(HWND(window), attributes, pl_attrs, web_context) + } + + pub fn new_as_child( + parent: &impl HasRawWindowHandle, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + web_context: Option<&mut WebContext>, + ) -> Result { + let parent = match parent.raw_window_handle() { + RawWindowHandle::Win32(parent) => parent.hwnd as _, + _ => return Err(Error::UnsupportedWindowHandle), + }; + + let class_name = encode_wide("WRY_WEBVIEW"); + + unsafe extern "system" fn default_window_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + DefWindowProcW(hwnd, msg, wparam, lparam) + } + + let class = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(default_window_proc), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: unsafe { HINSTANCE(GetModuleHandleW(PCWSTR::null()).unwrap_or_default().0) }, + hIcon: HICON::default(), + hCursor: HCURSOR::default(), // must be null in order for cursor state to work properly + hbrBackground: HBRUSH::default(), + lpszMenuName: PCWSTR::null(), + lpszClassName: PCWSTR::from_raw(class_name.as_ptr()), + hIconSm: HICON::default(), + }; + + unsafe { RegisterClassExW(&class) }; + + let mut flags = WS_CHILD | WS_CLIPCHILDREN; + if attributes.visible { + flags |= WS_VISIBLE; + } + + let child = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE::default(), + PCWSTR::from_raw(class_name.as_ptr()), + PCWSTR::null(), + flags, + attributes.position.map(|a| a.0).unwrap_or(CW_USEDEFAULT), + attributes.position.map(|a| a.1).unwrap_or(CW_USEDEFAULT), + attributes.size.map(|a| a.0 as i32).unwrap_or(CW_USEDEFAULT), + attributes.size.map(|a| a.1 as i32).unwrap_or(CW_USEDEFAULT), + HWND(parent), + HMENU::default(), + GetModuleHandleW(PCWSTR::null()).unwrap_or_default(), + None, + ) + }; + + Self::new_as_child_hwnd(child, attributes, pl_attrs, web_context) + } + + fn new_as_child_hwnd( + hwnd: HWND, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + web_context: Option<&mut WebContext>, + ) -> Result { + Self::new_hwnd(hwnd, attributes, pl_attrs, web_context).map(|mut w| { + w.is_child = true; + w + }) + } + + fn new_hwnd( + hwnd: HWND, mut attributes: WebViewAttributes, pl_attrs: super::PlatformSpecificWebViewAttributes, web_context: Option<&mut WebContext>, ) -> Result { - let hwnd = HWND(window.hwnd() as _); - let file_drop_controller: Rc> = Rc::new(OnceCell::new()); - let file_drop_handler = attributes.file_drop_handler.take(); - let file_drop_window = window.clone(); + let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; + + let file_drop_controller = attributes + .file_drop_handler + .take() + .map(|handler| FileDropController::new(hwnd, handler)); let env = Self::create_environment(&web_context, pl_attrs.clone(), &attributes)?; let controller = Self::create_controller(hwnd, &env, attributes.incognito)?; - let webview = Self::init_webview(window, hwnd, attributes, &env, &controller, pl_attrs)?; - - if let Some(file_drop_handler) = file_drop_handler { - let mut controller = FileDropController::new(); - controller.listen(hwnd, file_drop_window, file_drop_handler); - let _ = file_drop_controller.set(controller); - } + let webview = Self::init_webview(hwnd, attributes, &env, &controller, pl_attrs)?; Ok(Self { + hwnd, controller, + is_child: false, webview, env, file_drop_controller, @@ -227,7 +316,6 @@ impl InnerWebView { } fn init_webview( - window: Rc, hwnd: HWND, mut attributes: WebViewAttributes, env: &ICoreWebView2Environment, @@ -312,7 +400,6 @@ impl InnerWebView { // document title changed handler if let Some(document_title_changed_handler) = attributes.document_title_changed_handler { - let window_ = window.clone(); unsafe { webview .add_DocumentTitleChanged( @@ -321,7 +408,7 @@ impl InnerWebView { if let Some(webview) = webview { webview.DocumentTitle(&mut title)?; let title = take_pwstr(title); - document_title_changed_handler(&window_, title); + document_title_changed_handler(title); } Ok(()) })), @@ -332,7 +419,7 @@ impl InnerWebView { } if let Some(on_page_load_handler) = attributes.on_page_load_handler { - let on_page_load_handler = Arc::new(on_page_load_handler); + let on_page_load_handler = Rc::new(on_page_load_handler); let on_page_load_handler_ = on_page_load_handler.clone(); unsafe { @@ -368,22 +455,13 @@ impl InnerWebView { Self::add_script_to_execute_on_document_created( &webview, String::from( - r#"Object.defineProperty(window, 'ipc', { - value: Object.freeze({postMessage:s=>window.chrome.webview.postMessage(s)}) -}); - -window.addEventListener('mousedown', (e) => { - if (e.buttons === 1) window.chrome.webview.postMessage('__WEBVIEW_LEFT_MOUSE_DOWN__') -}); -window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('__WEBVIEW_MOUSE_MOVE__'));"#, + r#"Object.defineProperty(window, 'ipc', { value: Object.freeze({ postMessage: s=> window.chrome.webview.postMessage(s) }) });"#, ), )?; for js in attributes.initialization_scripts { Self::add_script_to_execute_on_document_created(&webview, js)?; } - let window_ = window.clone(); - // Message handler let ipc_handler = attributes.ipc_handler.take(); unsafe { @@ -393,49 +471,8 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ let mut js = PWSTR::null(); args.TryGetWebMessageAsString(&mut js)?; let js = take_pwstr(js); - if js == "__WEBVIEW_LEFT_MOUSE_DOWN__" || js == "__WEBVIEW_MOUSE_MOVE__" { - if !window_.is_decorated() && window_.is_resizable() && !window_.is_maximized() { - use crate::application::window::CursorIcon; - - let mut point = POINT::default(); - win32wm::GetCursorPos(&mut point)?; - let result = resize::hit_test(window_.hwnd(), point.x, point.y); - let cursor = match result.0 as u32 { - win32wm::HTLEFT => CursorIcon::WResize, - win32wm::HTTOP => CursorIcon::NResize, - win32wm::HTRIGHT => CursorIcon::EResize, - win32wm::HTBOTTOM => CursorIcon::SResize, - win32wm::HTTOPLEFT => CursorIcon::NwResize, - win32wm::HTTOPRIGHT => CursorIcon::NeResize, - win32wm::HTBOTTOMLEFT => CursorIcon::SwResize, - win32wm::HTBOTTOMRIGHT => CursorIcon::SeResize, - _ => CursorIcon::Arrow, - }; - // don't use `CursorIcon::Arrow` variant or cursor manipulation using css will cause cursor flickering - if cursor != CursorIcon::Arrow { - window_.set_cursor_icon(cursor); - } - - if js == "__WEBVIEW_LEFT_MOUSE_DOWN__" { - // we ignore `HTCLIENT` variant so the webview receives the click correctly if it is not on the edges - // and prevent conflict with `tao::window::drag_window`. - if result.0 as u32 != win32wm::HTCLIENT { - resize::begin_resize_drag( - window_.hwnd(), - result.0, - win32wm::WM_NCLBUTTONDOWN, - point.x, - point.y, - )?; - } - } - } - // these are internal messages, ipc_handlers don't need it so exit early - return Ok(()); - } - if let Some(ipc_handler) = &ipc_handler { - ipc_handler(&window_, js); + ipc_handler(js); } } @@ -589,7 +626,6 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ let env = env.clone(); let main_thread_id = std::thread::current().id(); - let hwnd = window.hwnd(); unsafe { webview .add_WebResourceRequested( @@ -852,7 +888,7 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ unsafe { controller - .SetIsVisible(true) + .SetIsVisible(attributes.visible) .map_err(webview2_com::Error::WindowsError)?; if attributes.focused { controller @@ -969,6 +1005,62 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ set_theme(&self.webview, theme); } + pub fn set_position(&self, position: (i32, i32)) { + if self.is_child { + unsafe { + let _ = SetWindowPos( + self.hwnd, + HWND::default(), + position.0, + position.1, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER, + ); + } + } + } + + pub fn set_size(&self, size: (u32, u32)) { + if self.is_child { + unsafe { + let _ = SetWindowPos( + self.hwnd, + HWND::default(), + 0, + 0, + size.0 as _, + size.1 as _, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER, + ); + } + } + } + + pub fn set_visible(&self, visible: bool) { + unsafe { + if self.is_child { + ShowWindow( + self.hwnd, + match visible { + true => SW_SHOW, + false => SW_HIDE, + }, + ); + } + + let _ = self.controller.SetIsVisible(visible); + } + } + + pub fn focus(&self) { + unsafe { + let _ = self + .controller + .MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); + } + } + pub fn set_memory_usage_level(&self, level: MemoryUsageLevel) { let Ok(webview) = self.webview.cast::() else { return; @@ -1125,11 +1217,8 @@ fn get_function_impl(library: &str, function: &str) -> Option { macro_rules! get_function { ($lib:expr, $func:ident) => { - crate::webview::webview2::get_function_impl( - concat!($lib, '\0'), - concat!(stringify!($func), '\0'), - ) - .map(|f| unsafe { std::mem::transmute::(f) }) + crate::webview2::get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')) + .map(|f| unsafe { std::mem::transmute::(f) }) }; } @@ -1171,7 +1260,7 @@ fn url_from_webview(webview: &ICoreWebView2) -> String { static EXEC_MSG_ID: Lazy = Lazy::new(|| unsafe { RegisterWindowMessageA(s!("Wry::ExecMsg")) }); -unsafe fn dispatch_handler(hwnd: isize, function: F) +unsafe fn dispatch_handler(hwnd: HWND, function: F) where F: FnMut() + 'static, { @@ -1181,7 +1270,7 @@ where let raw = Box::into_raw(boxed2); - let res = PostMessageW(HWND(hwnd), *EXEC_MSG_ID, WPARAM(raw as _), LPARAM(0)); + let res = PostMessageW(hwnd, *EXEC_MSG_ID, WPARAM(raw as _), LPARAM(0)); assert!( res.is_ok(), "PostMessage failed ; is the messages queue full?" diff --git a/src/webview/wkwebview/download.rs b/src/wkwebview/download.rs similarity index 100% rename from src/webview/wkwebview/download.rs rename to src/wkwebview/download.rs diff --git a/src/webview/wkwebview/file_drop.rs b/src/wkwebview/file_drop.rs similarity index 78% rename from src/webview/wkwebview/file_drop.rs rename to src/wkwebview/file_drop.rs index 36992eb92..33e6fc38a 100644 --- a/src/webview/wkwebview/file_drop.rs +++ b/src/wkwebview/file_drop.rs @@ -5,7 +5,6 @@ use std::{ ffi::{c_void, CStr}, path::PathBuf, - rc::Rc, }; use cocoa::{ @@ -18,10 +17,7 @@ use objc::{ }; use once_cell::sync::Lazy; -use crate::{ - application::{dpi::LogicalPosition, platform::macos::WindowExtMacOS, window::Window}, - webview::FileDropEvent, -}; +use crate::FileDropEvent; pub(crate) type NSDragOperation = cocoa::foundation::NSUInteger; #[allow(non_upper_case_globals)] @@ -61,20 +57,17 @@ static OBJC_DRAGGING_UPDATED: Lazy NSDr // Safety: objc runtime calls are unsafe pub(crate) unsafe fn set_file_drop_handler( webview: *mut Object, - window: Rc, - handler: Box bool>, -) -> *mut (Box bool>, Rc) { - let listener = Box::into_raw(Box::new((handler, window))); + handler: Box bool>, +) -> *mut Box bool> { + let listener = Box::into_raw(Box::new(handler)); (*webview).set_ivar("FileDropHandler", listener as *mut _ as *mut c_void); listener } #[allow(clippy::mut_from_ref)] -unsafe fn get_handler( - this: &Object, -) -> &mut (Box bool>, Rc) { +unsafe fn get_handler(this: &Object) -> &mut Box bool> { let delegate: *mut c_void = *this.get_ivar("FileDropHandler"); - &mut *(delegate as *mut (Box bool>, Rc)) + &mut *(delegate as *mut Box bool>) } unsafe fn collect_paths(drag_info: id) -> Vec { @@ -116,13 +109,10 @@ extern "C" fn dragging_entered(this: &mut Object, sel: Sel, drag_info: id) -> NS let paths = unsafe { collect_paths(drag_info) }; let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] }; - let scale_factor = listener.1.scale_factor(); - let ns_window = listener.1.ns_window() as id; - let frame: NSRect = unsafe { msg_send![ns_window, frame] }; - let position = - LogicalPosition::::from((dl.x, frame.size.height - dl.y)).to_physical(scale_factor); + let frame: NSRect = unsafe { msg_send![this, frame] }; + let position = (dl.x as i32, (frame.size.height - dl.y) as i32); - if !listener.0(&listener.1, FileDropEvent::Hovered { paths, position }) { + if !listener(FileDropEvent::Hovered { paths, position }) { // Reject the Wry file drop (invoke the OS default behaviour) OBJC_DRAGGING_ENTERED(this, sel, drag_info) } else { @@ -135,13 +125,10 @@ extern "C" fn perform_drag_operation(this: &mut Object, sel: Sel, drag_info: id) let paths = unsafe { collect_paths(drag_info) }; let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] }; - let scale_factor = listener.1.scale_factor(); - let ns_window = listener.1.ns_window() as id; - let frame: NSRect = unsafe { msg_send![ns_window, frame] }; - let position = - LogicalPosition::::from((dl.x, frame.size.height - dl.y)).to_physical(scale_factor); + let frame: NSRect = unsafe { msg_send![this, frame] }; + let position = (dl.x as i32, (frame.size.height - dl.y) as i32); - if !listener.0(&listener.1, FileDropEvent::Dropped { paths, position }) { + if !listener(FileDropEvent::Dropped { paths, position }) { // Reject the Wry file drop (invoke the OS default behaviour) OBJC_PERFORM_DRAG_OPERATION(this, sel, drag_info) } else { @@ -151,7 +138,7 @@ extern "C" fn perform_drag_operation(this: &mut Object, sel: Sel, drag_info: id) extern "C" fn dragging_exited(this: &mut Object, sel: Sel, drag_info: id) { let listener = unsafe { get_handler(this) }; - if !listener.0(&listener.1, FileDropEvent::Cancelled) { + if !listener(FileDropEvent::Cancelled) { // Reject the Wry file drop (invoke the OS default behaviour) OBJC_DRAGGING_EXITED(this, sel, drag_info); } diff --git a/src/webview/wkwebview/mod.rs b/src/wkwebview/mod.rs similarity index 86% rename from src/webview/wkwebview/mod.rs rename to src/wkwebview/mod.rs index a070a2d36..b13291dbc 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -11,42 +11,36 @@ mod proxy; #[cfg(target_os = "macos")] mod synthetic_mouse_events; -use url::Url; - #[cfg(target_os = "macos")] -use cocoa::appkit::{NSView, NSViewHeightSizable, NSViewWidthSizable}; +use cocoa::appkit::{NSView, NSViewHeightSizable, NSViewMinYMargin, NSViewWidthSizable}; use cocoa::{ base::{id, nil, NO, YES}, foundation::{NSDictionary, NSFastEnumeration, NSInteger}, }; +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; +use url::Url; use std::{ borrow::Cow, ffi::{c_void, CStr}, os::raw::c_char, ptr::{null, null_mut}, - rc::Rc, slice, str, sync::{Arc, Mutex}, }; -use core_graphics::geometry::CGRect; +use core_graphics::geometry::{CGPoint, CGRect, CGSize}; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel, BOOL}, }; use objc_id::Id; -#[cfg(target_os = "macos")] -use crate::application::platform::macos::WindowExtMacOS; #[cfg(target_os = "macos")] use file_drop::{add_file_drop_methods, set_file_drop_handler}; -#[cfg(target_os = "ios")] -use crate::application::platform::ios::WindowExtIOS; - #[cfg(feature = "mac-proxy")] -use crate::webview::{ +use crate::{ proxy::ProxyConfig, wkwebview::proxy::{ nw_endpoint_t, nw_proxy_config_create_http_connect, nw_proxy_config_create_socksv5, @@ -54,21 +48,14 @@ use crate::webview::{ }; use crate::{ - application::{ - dpi::{LogicalSize, PhysicalSize}, - window::Window, - }, - webview::{ - wkwebview::{ - download::{ - add_download_methods, download_did_fail, download_did_finish, download_policy, - set_download_delegate, - }, - navigation::{add_navigation_mathods, drop_navigation_methods, set_navigation_methods}, + wkwebview::{ + download::{ + add_download_methods, download_did_fail, download_did_finish, download_policy, + set_download_delegate, }, - FileDropEvent, PageLoadEvent, RequestAsyncResponder, WebContext, WebViewAttributes, RGBA, + navigation::{add_navigation_mathods, drop_navigation_methods, set_navigation_methods}, }, - Result, + Error, PageLoadEvent, RequestAsyncResponder, Result, WebContext, WebViewAttributes, RGBA, }; use http::{ @@ -79,6 +66,7 @@ use http::{ }; const IPC_MESSAGE_HANDLER_NAME: &str = "ipc"; +#[cfg(target_os = "macos")] const ACCEPT_FIRST_MOUSE: &str = "accept_first_mouse"; const NS_JSON_WRITING_FRAGMENTS_ALLOWED: u64 = 4; @@ -88,25 +76,61 @@ pub(crate) struct InnerWebView { #[cfg(target_os = "macos")] pub ns_window: id, pub manager: id, + is_child: bool, pending_scripts: Arc>>>, // Note that if following functions signatures are changed in the future, // all functions pointer declarations in objc callbacks below all need to get updated. - ipc_handler_ptr: *mut (Box, Rc), - document_title_changed_handler: *mut (Box, Rc), + ipc_handler_ptr: *mut Box, + document_title_changed_handler: *mut Box, navigation_decide_policy_ptr: *mut Box bool>, page_load_handler: *mut Box, #[cfg(target_os = "macos")] - file_drop_ptr: *mut (Box bool>, Rc), + file_drop_ptr: *mut Box bool>, download_delegate: id, protocol_ptrs: Vec<*mut Box>, RequestAsyncResponder)>>, } impl InnerWebView { pub fn new( - window: Rc, + window: &impl HasRawWindowHandle, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + _web_context: Option<&mut WebContext>, + ) -> Result { + let ns_view = match window.raw_window_handle() { + #[cfg(target_os = "macos")] + RawWindowHandle::AppKit(w) => w.ns_view, + #[cfg(target_os = "ios")] + RawWindowHandle::UiKit(w) => w.ui_view, + _ => return Err(Error::UnsupportedWindowHandle), + }; + + Self::new_ns_view(ns_view as _, attributes, pl_attrs, _web_context, false) + } + + pub fn new_as_child( + window: &impl HasRawWindowHandle, + attributes: WebViewAttributes, + pl_attrs: super::PlatformSpecificWebViewAttributes, + _web_context: Option<&mut WebContext>, + ) -> Result { + let ns_view = match window.raw_window_handle() { + #[cfg(target_os = "macos")] + RawWindowHandle::AppKit(w) => w.ns_view, + #[cfg(target_os = "ios")] + RawWindowHandle::UiKit(w) => w.ui_view, + _ => return Err(Error::UnsupportedWindowHandle), + }; + + Self::new_ns_view(ns_view as _, attributes, pl_attrs, _web_context, true) + } + + fn new_ns_view( + ns_view: id, attributes: WebViewAttributes, _pl_attrs: super::PlatformSpecificWebViewAttributes, _web_context: Option<&mut WebContext>, + is_child: bool, ) -> Result { // Function for ipc handler extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) { @@ -114,13 +138,12 @@ impl InnerWebView { unsafe { let function = this.get_ivar::<*mut c_void>("function"); if !function.is_null() { - let function = - &mut *(*function as *mut (Box Fn(&'r Window, String)>, Rc)); + let function = &mut *(*function as *mut Box); let body: id = msg_send![msg, body]; let utf8: *const c_char = msg_send![body, UTF8String]; let js = CStr::from_ptr(utf8).to_str().expect("Invalid UTF8 string"); - (function.0)(&function.1, js.to_string()); + (function)(js.to_string()); } else { log::warn!("WebView instance is dropped! This handler shouldn't be called."); } @@ -371,17 +394,37 @@ impl InnerWebView { #[cfg(target_os = "macos")] { - use core_graphics::geometry::{CGPoint, CGSize}; - let frame: CGRect = CGRect::new(&CGPoint::new(0., 0.), &CGSize::new(0., 0.)); + let (x, y) = attributes.position.unwrap_or((0, 0)); + + let (w, h) = attributes.size.unwrap_or_else(|| { + if is_child { + let frame = NSView::frame(ns_view); + (frame.size.width as u32, frame.size.height as u32) + } else { + (0, 0) + } + }); + let frame: CGRect = CGRect::new( + &window_position( + if is_child { ns_view } else { webview }, + (x, y), + (w as f64, h as f64), + ), + &CGSize::new(w as f64, h as f64), + ); let _: () = msg_send![webview, initWithFrame:frame configuration:config]; - // Auto-resize on macOS - webview.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable); + if is_child { + // fixed element + webview.setAutoresizingMask_(NSViewMinYMargin); + } else { + // Auto-resize + webview.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable); + } } #[cfg(target_os = "ios")] { - let ui_view = window.ui_view() as id; - let frame: CGRect = msg_send![ui_view, frame]; + let frame: CGRect = msg_send![ns_view, frame]; // set all autoresizingmasks let () = msg_send![webview, setAutoresizingMask: 31]; let _: () = msg_send![webview, initWithFrame:frame configuration:config]; @@ -391,6 +434,10 @@ impl InnerWebView { let _: () = msg_send![scroll, setBounces: NO]; } + if !attributes.visible { + let () = msg_send![webview, setHidden: YES]; + } + #[cfg(any(debug_assertions, feature = "devtools"))] if attributes.devtools { let has_inspectable_property: BOOL = @@ -425,7 +472,7 @@ impl InnerWebView { None => class!(WebViewDelegate), }; let handler: id = msg_send![cls, new]; - let ipc_handler_ptr = Box::into_raw(Box::new((ipc_handler, window.clone()))); + let ipc_handler_ptr = Box::into_raw(Box::new(ipc_handler)); (*handler).set_ivar("function", ipc_handler_ptr as *mut _ as *mut c_void); let ipc = NSString::new(IPC_MESSAGE_HANDLER_NAME); @@ -460,10 +507,9 @@ impl InnerWebView { unsafe { let function = this.get_ivar::<*mut c_void>("function"); if !function.is_null() { - let function = &mut *(*function - as *mut (Box Fn(&'r Window, String)>, Rc)); + let function = &mut *(*function as *mut Box); let title: id = msg_send![of_object, title]; - (function.0)(&function.1, NSString(title).to_str().to_string()); + (function)(NSString(title).to_str().to_string()); } } } @@ -475,7 +521,7 @@ impl InnerWebView { let handler: id = msg_send![cls, new]; let document_title_changed_handler = - Box::into_raw(Box::new((document_title_changed_handler, window.clone()))); + Box::into_raw(Box::new(document_title_changed_handler)); (*handler).set_ivar( "function", @@ -752,17 +798,15 @@ impl InnerWebView { #[cfg(target_os = "macos")] let file_drop_ptr = match attributes.file_drop_handler { // if we have a file_drop_handler defined, use the defined handler - Some(file_drop_handler) => { - set_file_drop_handler(webview, window.clone(), file_drop_handler) - } + Some(file_drop_handler) => set_file_drop_handler(webview, file_drop_handler), // prevent panic by using a blank handler - None => set_file_drop_handler(webview, window.clone(), Box::new(|_, _| false)), + None => set_file_drop_handler(webview, Box::new(|_| false)), }; // ns window is required for the print operation #[cfg(target_os = "macos")] let ns_window = { - let ns_window = window.ns_window() as id; + let ns_window: id = msg_send![ns_view, window]; let can_set_titlebar_style: BOOL = msg_send![ ns_window, @@ -790,6 +834,7 @@ impl InnerWebView { page_load_handler, download_delegate, protocol_ptrs, + is_child, }; // Initialize scripts @@ -825,37 +870,41 @@ r#"Object.defineProperty(window, 'ipc', { // Inject the web view into the window as main content #[cfg(target_os = "macos")] { - let parent_view_cls = match ClassDecl::new("WryWebViewParent", class!(NSView)) { - Some(mut decl) => { - decl.add_method( - sel!(keyDown:), - key_down as extern "C" fn(&mut Object, Sel, id), - ); + if is_child { + let _: () = msg_send![ns_view, addSubview: webview]; + } else { + let parent_view_cls = match ClassDecl::new("WryWebViewParent", class!(NSView)) { + Some(mut decl) => { + decl.add_method( + sel!(keyDown:), + key_down as extern "C" fn(&mut Object, Sel, id), + ); - extern "C" fn key_down(_this: &mut Object, _sel: Sel, event: id) { - unsafe { - let app = cocoa::appkit::NSApp(); - let menu: id = msg_send![app, mainMenu]; - let () = msg_send![menu, performKeyEquivalent: event]; + extern "C" fn key_down(_this: &mut Object, _sel: Sel, event: id) { + unsafe { + let app = cocoa::appkit::NSApp(); + let menu: id = msg_send![app, mainMenu]; + let () = msg_send![menu, performKeyEquivalent: event]; + } } - } - - decl.register() - } - None => class!(NSView), - }; - let parent_view: id = msg_send![parent_view_cls, alloc]; - let _: () = msg_send![parent_view, init]; - parent_view.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable); - let _: () = msg_send![parent_view, addSubview: webview]; + decl.register() + } + None => class!(NSView), + }; - // inject the webview into the window - let ns_window = window.ns_window() as id; - // Tell the webview receive keyboard events in the window. - // See https://github.com/tauri-apps/wry/issues/739 - let _: () = msg_send![ns_window, setContentView: parent_view]; - let _: () = msg_send![ns_window, makeFirstResponder: webview]; + let parent_view: id = msg_send![parent_view_cls, alloc]; + let _: () = msg_send![parent_view, init]; + parent_view.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable); + let _: () = msg_send![parent_view, addSubview: webview]; + + // inject the webview into the window + let ns_window: id = msg_send![ns_view, window]; + // Tell the webview receive keyboard events in the window. + // See https://github.com/tauri-apps/wry/issues/739 + let _: () = msg_send![ns_window, setContentView: parent_view]; + let _: () = msg_send![ns_window, makeFirstResponder: webview]; + } // make sure the window is always on top when we create a new webview let app_class = class!(NSApplication); @@ -865,8 +914,7 @@ r#"Object.defineProperty(window, 'ipc', { #[cfg(target_os = "ios")] { - let ui_view = window.ui_view() as id; - let _: () = msg_send![ui_view, addSubview: webview]; + let _: () = msg_send![ns_view, addSubview: webview]; } Ok(w) @@ -1030,13 +1078,6 @@ r#"Object.defineProperty(window, 'ipc', { false } - #[cfg(target_os = "macos")] - pub fn inner_size(&self, scale_factor: f64) -> PhysicalSize { - let view_frame = unsafe { NSView::frame(self.webview) }; - let logical: LogicalSize = (view_frame.size.width, view_frame.size.height).into(); - logical.to_physical(scale_factor) - } - pub fn zoom(&self, scale_factor: f64) { unsafe { let _: () = msg_send![self.webview, setPageZoom: scale_factor]; @@ -1046,6 +1087,43 @@ r#"Object.defineProperty(window, 'ipc', { pub fn set_background_color(&self, _background_color: RGBA) -> Result<()> { Ok(()) } + + pub fn set_position(&self, position: (i32, i32)) { + if self.is_child { + unsafe { + let mut frame: CGRect = msg_send![self.webview, frame]; + frame.origin = window_position( + msg_send![self.webview, superview], + (position.0, position.1), + (frame.size.width, frame.size.height), + ); + let () = msg_send![self.webview, setFrame: frame]; + } + } + } + + pub fn set_size(&self, size: (u32, u32)) { + if self.is_child { + unsafe { + let mut frame: CGRect = msg_send![self.webview, frame]; + frame.size = CGSize::new(size.0 as f64, size.1 as f64); + let () = msg_send![self.webview, setFrame: frame]; + } + } + } + + pub fn set_visible(&self, visible: bool) { + unsafe { + let () = msg_send![self.webview, setHidden: !visible]; + } + } + + pub fn focus(&self) { + unsafe { + let window: id = msg_send![self.webview, window]; + let _: () = msg_send![window, makeFirstResponder: self.webview]; + } + } } pub fn url_from_webview(webview: id) -> String { @@ -1179,3 +1257,14 @@ impl From for NSString { } struct NSData(id); + +/// Converts from wry screen-coordinates to macOS screen-coordinates. +/// wry: top-left is (0, 0) and y increasing downwards +/// macOS: bottom-left is (0, 0) and y increasing upwards +unsafe fn window_position(view: id, position: (i32, i32), size: (f64, f64)) -> CGPoint { + let frame: CGRect = msg_send![view, frame]; + CGPoint::new( + position.0 as f64, + frame.size.height - position.1 as f64 - size.1, + ) +} diff --git a/src/webview/wkwebview/navigation.rs b/src/wkwebview/navigation.rs similarity index 98% rename from src/webview/wkwebview/navigation.rs rename to src/wkwebview/navigation.rs index b66bce591..10579f331 100644 --- a/src/webview/wkwebview/navigation.rs +++ b/src/wkwebview/navigation.rs @@ -11,7 +11,7 @@ use objc::{ }; use super::{url_from_webview, InnerWebView, NSString}; -use crate::webview::PageLoadEvent; +use crate::PageLoadEvent; extern "C" fn did_commit_navigation(this: &Object, _: Sel, webview: id, _navigation: id) { unsafe { diff --git a/src/webview/wkwebview/proxy.rs b/src/wkwebview/proxy.rs similarity index 97% rename from src/webview/wkwebview/proxy.rs rename to src/wkwebview/proxy.rs index b2c1f46a2..8a41fd84d 100644 --- a/src/webview/wkwebview/proxy.rs +++ b/src/wkwebview/proxy.rs @@ -5,7 +5,7 @@ use cocoa::base::nil; use libc::c_char; -use crate::{webview::proxy::ProxyEndpoint, Error}; +use crate::{proxy::ProxyEndpoint, Error}; use super::NSString; diff --git a/src/webview/wkwebview/synthetic_mouse_events.rs b/src/wkwebview/synthetic_mouse_events.rs similarity index 100% rename from src/webview/wkwebview/synthetic_mouse_events.rs rename to src/wkwebview/synthetic_mouse_events.rs