diff --git a/.changes/15.md b/.changes/15.md new file mode 100644 index 0000000..deb2855 --- /dev/null +++ b/.changes/15.md @@ -0,0 +1,5 @@ +--- +"wry": minor +--- + +Update gtk to 0.15 diff --git a/.changes/clipboard.md b/.changes/clipboard.md new file mode 100644 index 0000000..7471ebb --- /dev/null +++ b/.changes/clipboard.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +Add clipboard field in WebViewAttributes. + diff --git a/.changes/fix-windows7-runtime.md b/.changes/fix-windows7-runtime.md new file mode 100644 index 0000000..2e39a95 --- /dev/null +++ b/.changes/fix-windows7-runtime.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Ignore transparency on Windows 7 to prevent application crash. diff --git a/.changes/linux-clipboard.md b/.changes/linux-clipboard.md new file mode 100644 index 0000000..966418a --- /dev/null +++ b/.changes/linux-clipboard.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Remove clipboard property for consistency across platforms. diff --git a/.changes/linux-cookie-storage.md b/.changes/linux-cookie-storage.md new file mode 100644 index 0000000..f45a207 --- /dev/null +++ b/.changes/linux-cookie-storage.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Enable cookie persistence on Linux if the `data_directory` is provided. diff --git a/.changes/mac-exception.md b/.changes/mac-exception.md new file mode 100644 index 0000000..23ea412 --- /dev/null +++ b/.changes/mac-exception.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +Enable objc's exception features so they can be treated as panic message. + diff --git a/.changes/mac-inner-size.md b/.changes/mac-inner-size.md new file mode 100644 index 0000000..a22e111 --- /dev/null +++ b/.changes/mac-inner-size.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Add inner size method for webview. This can reflect correct size of webview on macOS. diff --git a/.changes/mac-priv.md b/.changes/mac-priv.md new file mode 100644 index 0000000..c9b10dc --- /dev/null +++ b/.changes/mac-priv.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +Add "transparent" and "fullscreen" featrue flags on macOS to toggle private API. + diff --git a/.changes/mac-webcontext.md b/.changes/mac-webcontext.md new file mode 100644 index 0000000..920d43f --- /dev/null +++ b/.changes/mac-webcontext.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +Implement WebContextImpl on mac to extend several callback lifetimes. + diff --git a/.changes/remove-shared-mod.md b/.changes/remove-shared-mod.md new file mode 100644 index 0000000..7370215 --- /dev/null +++ b/.changes/remove-shared-mod.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +The only thing that private mod shared does is re-export http mod to public, +we can just pub mod http. diff --git a/.changes/resizing-undecorated-window-touch.md b/.changes/resizing-undecorated-window-touch.md new file mode 100644 index 0000000..660fcd0 --- /dev/null +++ b/.changes/resizing-undecorated-window-touch.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +* Fix hovering over an edge of undecorated window on Linux won't change cursor. +* Undecorated window can be resized using touch on Linux. \ No newline at end of file diff --git a/.changes/webkit2gtk.md b/.changes/webkit2gtk.md new file mode 100644 index 0000000..d5bb720 --- /dev/null +++ b/.changes/webkit2gtk.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +Update webkit2gtk to 0.15 + diff --git a/.changes/webview-user-agent.md b/.changes/webview-user-agent.md new file mode 100644 index 0000000..95119f6 --- /dev/null +++ b/.changes/webview-user-agent.md @@ -0,0 +1,4 @@ +--- +"wry": minor +--- +Add `with_user_agent(&str)` to `WebViewBuilder`. diff --git a/.changes/webview2-com.md b/.changes/webview2-com.md new file mode 100644 index 0000000..8717564 --- /dev/null +++ b/.changes/webview2-com.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Replace all of the `winapi` crate references with the `windows` crate, and replace `webview2` and `webview2-sys` with `webview2-com` and `webview2-com-sys` built with the `windows` crate. The replacement bindings are in the `webview2-com-sys` crate, with `pub use` in the `webview2-com` crate. They can be shared with TAO. \ No newline at end of file diff --git a/.changes/webview2-null.md b/.changes/webview2-null.md new file mode 100644 index 0000000..1296c7a --- /dev/null +++ b/.changes/webview2-null.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +Fix null pointer crash on `get_content` of web resource request. This is a temporary fix. +We will switch it back once upstream is updated. \ No newline at end of file diff --git a/.changes/windows-0.25.0.md b/.changes/windows-0.25.0.md new file mode 100644 index 0000000..9bbc762 --- /dev/null +++ b/.changes/windows-0.25.0.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Update the `windows` crate to 0.25.0, which comes with pre-built libraries. WRY and Tao can both reference the same types directly from the `windows` crate instead of sharing bindings in `webview2-com-sys`. \ No newline at end of file diff --git a/.changes/windows-0.29.0.md b/.changes/windows-0.29.0.md new file mode 100644 index 0000000..0ebc298 --- /dev/null +++ b/.changes/windows-0.29.0.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Update the `windows` crate to 0.29.0 and `webview2-com` to 0.9.0. \ No newline at end of file diff --git a/.changes/windows-0.30.0.md b/.changes/windows-0.30.0.md new file mode 100644 index 0000000..df73b36 --- /dev/null +++ b/.changes/windows-0.30.0.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Update the `windows` crate to 0.30.0 and `webview2-com` to 0.10.0. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3a2675c..7cb3c1f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,36 +1,30 @@ - - - -**What kind of change does this PR introduce?** (check at least one) +### What kind of change does this PR introduce? + - [ ] Bugfix - [ ] Feature +- [ ] Docs +- [ ] New Binding issue #___ - [ ] Code style update - [ ] Refactor -- [ ] Documentation - [ ] Build-related changes - [ ] Other, please describe: -**Does this PR introduce a breaking change?** (check one) - +### Does this PR introduce a breaking change? + -- [ ] Yes. Issue #___ +- [ ] Yes, and the changes were approved in issue #___ - [ ] No - -**The PR fulfills these requirements:** - -- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix: #xxx[,#xxx]`, where "xxx" is the issue number) +### Checklist +- [ ] When resolving issues, they are referenced in the PR's title (e.g `fix: remove a typo, closes #___, #___`) - [ ] A change file is added if any packages will require a version bump due to this PR per [the instructions in the readme](https://github.com/tauri-apps/tauri/blob/dev/.changes/readme.md). +- [ ] I have added a convincing reason for adding this feature, if necessary -If adding a **new feature**, the PR's description includes: -- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it) - -**Other information:** +### Other information diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7300afd..2fbd4b2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,12 +74,12 @@ jobs: ${{ matrix.platform }}-stable-cargo-core- - name: build wry - run: cargo build --target ${{ matrix.platform.target }} + run: cargo build --features tray --target ${{ matrix.platform.target }} - name: build tests and examples shell: bash - run: cargo test --no-run --verbose --target ${{ matrix.platform.target }} + run: cargo test --no-run --verbose --features tray --target ${{ matrix.platform.target }} - name: run tests if: (!contains(matrix.platform.target, 'ios')) - run: cargo test --verbose --target ${{ matrix.platform.target }} + run: cargo test --verbose --features tray --target ${{ matrix.platform.target }} diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index 25b0126..7185736 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -15,14 +15,9 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly + toolchain: stable override: true - components: rustfmt, clippy - - name: clippy check - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-targets -- -D warnings + components: rustfmt - name: fmt uses: actions-rs/cargo@v1 with: diff --git a/Cargo.toml b/Cargo.toml index b1a23e0..482ea85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ workspace = { } name = "wry" version = "0.12.2" authors = [ "Tauri Programme within The Commons Conservancy" ] -edition = "2018" +edition = "2021" license = "Apache-2.0 OR MIT" description = "Cross-platform WebView rendering library" readme = "README.md" @@ -22,11 +22,14 @@ targets = [ ] [features] -default = [ "file-drop", "protocol", "tray" ] +default = [ "file-drop", "protocol" ] file-drop = [ ] protocol = [ ] dox = [ "tao/dox" ] tray = [ "tao/tray" ] +ayatana = [ "tao/ayatana" ] +transparent = [ ] +fullscreen = [ ] [dependencies] libc = "0.2" @@ -36,30 +39,43 @@ serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" thiserror = "1.0" url = "2.2" -tao = { version = "0.5.2", default-features = false, features = [ "serde" ] } +tao = { version = "0.6", default-features = false, features = [ "serde" ] } http = "0.2.5" [dev-dependencies] anyhow = "1.0.43" -chrono = "0.4.19" tempfile = "3.2.0" http-range = "0.1.4" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] -webkit2gtk = { version = "0.14", features = [ "v2_18" ] } -webkit2gtk-sys = "0.14" -gio = "0.14" -glib = "0.14" -gtk = "0.14" -gdk = "0.14" +webkit2gtk = { version = "0.17", features = [ "v2_22" ] } +webkit2gtk-sys = "0.17" +gio = "0.15" +glib = "0.15" +gtk = "0.15" +gdk = "0.15" [target."cfg(target_os = \"windows\")".dependencies] -webview2 = "0.1" -webview2-sys = "0.1" -winapi = { version = "0.3", features = [ "libloaderapi", "oleidl" ] } +webview2-com = "0.10.0" +windows_macros = "0.30.0" +sys-info = "0.9" + +[target."cfg(target_os = \"windows\")".dependencies.windows] +version = "0.30.0" +features = [ + "alloc", + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_System_Com", + "Win32_System_Com_StructuredStorage", + "Win32_System_Ole", + "Win32_System_SystemServices", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", +] [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies] cocoa = "0.24" core-graphics = "0.22" -objc = "0.2" +objc = { version = "0.2", features = ["exception"] } objc_id = "0.1" diff --git a/README.md b/README.md index 45e7fc9..03feaa8 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ For more information, please read the documentation below. ## Platform-specific notes -All platforms uses [tao](https://github.com/rust-windowing/tao) to build the window, and wry re-export it as application module. Here are the underlying web engine each platform uses, and some dependencies you might need to install. +All platforms uses [tao](https://github.com/tauri-apps/tao) to build the window, and wry re-export it as application module. Here are the underlying web engine each platform uses, and some dependencies you might need to install. ### Linux diff --git a/audits/Radically_Open_Security-v1-report.pdf b/audits/Radically_Open_Security-v1-report.pdf new file mode 100644 index 0000000..743d8be Binary files /dev/null and b/audits/Radically_Open_Security-v1-report.pdf differ diff --git a/bench/Cargo.toml b/bench/Cargo.toml index b41edcd..c2d3abc 100644 --- a/bench/Cargo.toml +++ b/bench/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/tauri-apps/wry" [dependencies] anyhow = "1.0.43" -chrono = "0.4.19" +time = "0.3" tempfile = "3.2.0" serde_json = "1.0" serde = { version = "1.0", features = [ "derive" ] } diff --git a/bench/src/run_benchmark.rs b/bench/src/run_benchmark.rs index 8b54196..0c8806e 100644 --- a/bench/src/run_benchmark.rs +++ b/bench/src/run_benchmark.rs @@ -259,7 +259,7 @@ fn main() -> Result<()> { env::set_current_dir(&utils::bench_root_path())?; let mut new_data = utils::BenchResult { - created_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true), + created_at: format!("{}", time::OffsetDateTime::now_utc()), sha1: utils::run_collect(&["git", "rev-parse", "HEAD"]) .0 .trim() diff --git a/bench/tests/src/cpu_intensive.rs b/bench/tests/src/cpu_intensive.rs index da0073a..c90c53a 100644 --- a/bench/tests/src/cpu_intensive.rs +++ b/bench/tests/src/cpu_intensive.rs @@ -12,17 +12,16 @@ fn main() -> wry::Result<()> { window::{Window, WindowBuilder}, }, http::ResponseBuilder, - webview::{RpcRequest, WebViewBuilder}, + webview::WebViewBuilder, }; let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); - let handler = |_window: &Window, req: RpcRequest| { - if &req.method == "process-complete" { + let handler = |_window: &Window, req: String| { + if &req == "process-complete" { exit(0); } - None }; let webview = WebViewBuilder::new(window) .unwrap() @@ -50,7 +49,7 @@ fn main() -> wry::Result<()> { ResponseBuilder::new().mimetype(mimetype).body(data) }) .with_url("wry.bench://")? - .with_rpc_handler(handler) + .with_ipc_handler(handler) .build()?; event_loop.run(move |event, _, control_flow| { @@ -67,3 +66,4 @@ fn main() -> wry::Result<()> { } }); } + diff --git a/bench/tests/src/custom_protocol.rs b/bench/tests/src/custom_protocol.rs index a5e6b22..f8b555c 100644 --- a/bench/tests/src/custom_protocol.rs +++ b/bench/tests/src/custom_protocol.rs @@ -18,21 +18,20 @@ fn main() -> wry::Result<()> { window::{Window, WindowBuilder}, }, http::ResponseBuilder, - webview::{RpcRequest, WebViewBuilder}, + webview::WebViewBuilder, }; let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); - let handler = |_window: &Window, req: RpcRequest| { - if &req.method == "dom-loaded" { + let handler = |_window: &Window, req: String| { + if &req == "dom-loaded" { exit(0); } - None }; let webview = WebViewBuilder::new(window) .unwrap() - .with_rpc_handler(handler) + .with_ipc_handler(handler) .with_custom_protocol("wry.bench".into(), move |_request| { let index_html = r#" @@ -46,7 +45,7 @@ fn main() -> wry::Result<()> {

Welcome to WRY!

diff --git a/bench/tests/src/hello_world.rs b/bench/tests/src/hello_world.rs index 624762e..ee1c06f 100644 --- a/bench/tests/src/hello_world.rs +++ b/bench/tests/src/hello_world.rs @@ -17,7 +17,7 @@ fn main() -> wry::Result<()> { event_loop::{ControlFlow, EventLoop}, window::{Window, WindowBuilder}, }, - webview::{RpcRequest, WebViewBuilder}, + webview::WebViewBuilder, }; let event_loop = EventLoop::new(); @@ -26,21 +26,20 @@ fn main() -> wry::Result<()> { let url = r#"data:text/html, "#; - let handler = |_window: &Window, req: RpcRequest| { - if &req.method == "dom-loaded" { + let handler = |_window: &Window, req: String| { + if &req == "dom-loaded" { exit(0); } - None }; let webview = WebViewBuilder::new(window) .unwrap() .with_url(url)? - .with_rpc_handler(handler) + .with_ipc_handler(handler) .build()?; event_loop.run(move |event, _, control_flow| { diff --git a/bench/tests/src/static/site.js b/bench/tests/src/static/site.js index a818642..3467a77 100644 --- a/bench/tests/src/static/site.js +++ b/bench/tests/src/static/site.js @@ -17,7 +17,7 @@ const onMessage = (message) => { if (message.data.status === "done") { // tell rust that we are done - rpc.call("process-complete"); + ipc.postMessage("process-complete"); } status.innerHTML = `${prefix} Found ${message.data.count} prime numbers in ${message.data.time}ms`; diff --git a/examples/README.md b/examples/README.md index f051173..104d878 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,7 +11,6 @@ Run the `cargo run --example ` to see how each example works. - `hello_world`: the basic example to show the types and methods to create an application. - `menu_bar`: uses a custom menu for the application in macOS and the Window and Linux/Windows. - `multi_window`: create the window dynamically even after the application is running. -- `rpc`: A RPC example to explain how to use the RPC handler and interact with it. - `stream_range`: read the incoming header from the custom protocol and return part of the data. [RFC7233](https://httpwg.org/specs/rfc7233.html#header.range) - `system_tray_no_menu`: open window on tray icon left click. - `system_tray`: sample tray application with different behaviours. diff --git a/examples/custom_titlebar.rs b/examples/custom_titlebar.rs index 1df596e..4c60dda 100644 --- a/examples/custom_titlebar.rs +++ b/examples/custom_titlebar.rs @@ -1,3 +1,5 @@ +use tao::window::WindowId; + // Copyright 2019-2021 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT @@ -9,10 +11,14 @@ fn main() -> wry::Result<()> { event_loop::{ControlFlow, EventLoop}, window::{Window, WindowBuilder}, }, - webview::{RpcRequest, WebViewBuilder}, + webview::WebViewBuilder, }; - let event_loop = EventLoop::new(); + enum UserEvents { + CloseWindow(WindowId), + } + + let event_loop = EventLoop::::with_user_event(); let mut webviews = std::collections::HashMap::new(); let window = WindowBuilder::new() .with_decorations(false) @@ -45,20 +51,20 @@ fn main() -> wry::Result<()> { let script = r#" (function () { window.addEventListener('DOMContentLoaded', (event) => { - document.getElementById('minimize').addEventListener('click', () => rpc.notify('minimize')); - document.getElementById('maximize').addEventListener('click', () => rpc.notify('maximize')); - document.getElementById('close').addEventListener('click', () => rpc.notify('close')); + document.getElementById('minimize').addEventListener('click', () => ipc.postMessage('minimize')); + document.getElementById('maximize').addEventListener('click', () => ipc.postMessage('maximize')); + document.getElementById('close').addEventListener('click', () => ipc.postMessage('close')); document.addEventListener('mousedown', (e) => { if (e.target.classList.contains('drag-region') && e.buttons === 1) { e.detail === 2 - ? window.rpc.notify('maximize') - : window.rpc.notify('drag_window'); + ? window.ipc.postMessage('maximize') + : window.ipc.postMessage('drag_window'); } }) document.addEventListener('touchstart', (e) => { if (e.target.classList.contains('drag-region')) { - window.rpc.notify('drag_window'); + window.ipc.postMessage('drag_window'); } }) @@ -100,50 +106,38 @@ fn main() -> wry::Result<()> { })(); "#; - let (window_tx, window_rx) = std::sync::mpsc::channel(); + let proxy = event_loop.create_proxy(); - let handler = move |window: &Window, req: RpcRequest| { - if req.method == "minimize" { + let handler = move |window: &Window, req: String| { + if req == "minimize" { window.set_minimized(true); } - if req.method == "maximize" { - if window.is_maximized() { - window.set_maximized(false); - } else { - window.set_maximized(true); - } + if req == "maximize" { + window.set_maximized(!window.is_maximized()); } - if req.method == "close" { - let _ = window_tx.send(window.id()); + if req == "close" { + let _ = proxy.send_event(UserEvents::CloseWindow(window.id())); } - if req.method == "drag_window" { + if req == "drag_window" { let _ = window.drag_window(); } - None }; let webview = WebViewBuilder::new(window) .unwrap() .with_url(url)? .with_initialization_script(script) - .with_rpc_handler(handler) + .with_ipc_handler(handler) .build()?; webviews.insert(webview.window().id(), webview); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; - if let Ok(id) = window_rx.try_recv() { - webviews.remove(&id); - if webviews.is_empty() { - *control_flow = ControlFlow::Exit - } - } - if let Event::WindowEvent { - event, window_id, .. - } = event - { - match event { + match event { + Event::WindowEvent { + event, window_id, .. + } => match event { WindowEvent::CloseRequested => { webviews.remove(&window_id); if webviews.is_empty() { @@ -154,7 +148,14 @@ fn main() -> wry::Result<()> { let _ = webviews[&window_id].resize(); } _ => (), + }, + Event::UserEvent(UserEvents::CloseWindow(id)) => { + webviews.remove(&id); + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } } + _ => (), } }); } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 195f537..007e263 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -16,7 +16,7 @@ fn main() -> wry::Result<()> { let window = WindowBuilder::new() .with_title("Hello World") .build(&event_loop)?; - let _webview = WebViewBuilder::new(window)? + let webview = WebViewBuilder::new(window)? .with_url("https://html5test.com")? .build()?; @@ -29,7 +29,9 @@ fn main() -> wry::Result<()> { event: WindowEvent::CloseRequested, .. } => *control_flow = ControlFlow::Exit, - _ => (), + _ => { + dbg!(webview.window().inner_size()); + } } }); } diff --git a/examples/multi_window.rs b/examples/multi_window.rs index 789da3d..f61589c 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -2,100 +2,106 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::{ - collections::HashMap, - time::{Duration, Instant}, -}; - -#[derive(Debug, Serialize, Deserialize)] -struct MessageParameters { - message: String, -} - fn main() -> wry::Result<()> { + use std::collections::HashMap; use wry::{ application::{ - dpi::PhysicalSize, event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::{Window, WindowBuilder}, + event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowBuilder, WindowId}, }, - webview::{RpcRequest, WebContext, WebViewBuilder}, + webview::{WebView, WebViewBuilder}, }; - let event_loop = EventLoop::new(); - let mut web_context = WebContext::default(); - let window1 = WindowBuilder::new().build(&event_loop).unwrap(); + enum UserEvents { + CloseWindow(WindowId), + NewWindow(), + } - let (window_tx, window_rx) = std::sync::mpsc::channel::(); - let handler = move |_window: &Window, req: RpcRequest| { - if &req.method == "openWindow" { - if let Some(params) = req.params { - if let Value::String(url) = ¶ms[0] { - let _ = window_tx.send(url.to_string()); - } + 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(UserEvents::NewWindow()); } - } - None - }; + "close" => { + let _ = proxy.send_event(UserEvents::CloseWindow(window.id())); + } + _ if req.starts_with("change-title") => { + let title = req.replace("change-title:", ""); + window.set_title(title.as_str()); + } + _ => {} + }; - let id = window1.id(); - let webview1 = WebViewBuilder::new(window1) - .unwrap() - .with_url("https://tauri.studio")? - .with_initialization_script( - r#"async function openWindow() { - await window.rpc.notify("openWindow", "https://i.imgur.com/x6tXcr9.gif"); - }"#, - ) - .with_rpc_handler(handler) - .with_web_context(&mut web_context) - .build()?; + let webview = WebViewBuilder::new(window) + .unwrap() + .with_html( + r#" + + + + "#, + ) + .unwrap() + .with_ipc_handler(handler) + .build() + .unwrap(); + (window_id, webview) + } + + let event_loop = EventLoop::::with_user_event(); let mut webviews = HashMap::new(); - webviews.insert(id, webview1); + 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); - let instant = Instant::now(); - let eight_secs = Duration::from_secs(8); - let mut trigger = true; event_loop.run(move |event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; - if let Ok(url) = window_rx.try_recv() { - let window2 = WindowBuilder::new() - .with_title("RODA RORA DA") - .with_inner_size(PhysicalSize::new(426, 197)) - .build(event_loop) - .unwrap(); - let id = window2.id(); - let webview2 = WebViewBuilder::new(window2) - .unwrap() - .with_url(&url) - .unwrap() - .with_web_context(&mut web_context) - .build() - .unwrap(); - webviews.insert(id, webview2); - } else if trigger && instant.elapsed() >= eight_secs { - webviews - .get_mut(&id) - .unwrap() - .evaluate_script("openWindow()") - .unwrap(); - trigger = false; - } - - if let Event::WindowEvent { - window_id, - event: WindowEvent::CloseRequested, - .. - } = event - { - webviews.remove(&window_id); - if webviews.is_empty() { - *control_flow = ControlFlow::Exit; + match event { + Event::WindowEvent { + event, window_id, .. + } => match event { + WindowEvent::CloseRequested => { + webviews.remove(&window_id); + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } + } + WindowEvent::Resized(_) => { + let _ = webviews[&window_id].resize(); + } + _ => (), + }, + Event::UserEvent(UserEvents::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(UserEvents::CloseWindow(id)) => { + webviews.remove(&id); + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } + } + _ => (), } }); } diff --git a/examples/rpc.rs b/examples/rpc.rs deleted file mode 100644 index e8d884a..0000000 --- a/examples/rpc.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2019-2021 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Debug, Serialize, Deserialize)] -struct MessageParameters { - message: String, -} - -fn main() -> wry::Result<()> { - use wry::{ - application::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::{Fullscreen, Window, WindowBuilder}, - }, - webview::{RpcRequest, RpcResponse, WebViewBuilder}, - }; - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); - - let url = r#"data:text/html, - -
-
-
-"#; - - let handler = |window: &Window, mut req: RpcRequest| { - let mut response = None; - if &req.method == "fullscreen" { - if let Some(params) = req.params.take() { - if let Ok(mut args) = serde_json::from_value::>(params) { - if !args.is_empty() { - if args.swap_remove(0) { - window.set_fullscreen(Some(Fullscreen::Borderless(None))); - } else { - window.set_fullscreen(None); - } - }; - response = Some(RpcResponse::new_result(req.id.take(), None)); - } - } - } else if &req.method == "send-parameters" { - if let Some(params) = req.params.take() { - if let Ok(mut args) = serde_json::from_value::>(params) { - let result = if !args.is_empty() { - let msg = args.swap_remove(0); - Some(Value::String(format!("Hello, {}!", msg.message))) - } else { - // NOTE: in the real-world we should send an error response here! - None - }; - // Must always send a response as this is a `call()` - response = Some(RpcResponse::new_result(req.id.take(), result)); - } - } - } - - response - }; - let webview = WebViewBuilder::new(window) - .unwrap() - .with_url(url)? - .with_rpc_handler(handler) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => { - let _ = webview.resize(); - } - } - }); -} diff --git a/examples/system_tray.rs b/examples/system_tray.rs index 68561ae..f8237a3 100644 --- a/examples/system_tray.rs +++ b/examples/system_tray.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] +#[cfg(any(feature = "tray", feature = "ayatana"))] fn main() -> wry::Result<()> { use std::collections::HashMap; #[cfg(target_os = "linux")] @@ -220,3 +221,10 @@ fn main() -> wry::Result<()> { fn main() { println!("This platform doesn't support system_tray."); } + +// Tray feature flag disabled but can be available. +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(not(any(feature = "tray", feature = "ayatana")))] +fn main() { + println!("This platform doesn't have the `tray` feature enabled."); +} diff --git a/examples/system_tray_no_menu.rs b/examples/system_tray_no_menu.rs index 0aa5fc4..e6b2ede 100644 --- a/examples/system_tray_no_menu.rs +++ b/examples/system_tray_no_menu.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] +#[cfg(any(feature = "tray", feature = "ayatana"))] fn main() -> wry::Result<()> { use std::collections::HashMap; #[cfg(target_os = "linux")] @@ -188,3 +189,10 @@ fn main() -> wry::Result<()> { fn main() { println!("This platform doesn't support system_tray."); } + +// Tray feature flag disabled but can be available. +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(not(any(feature = "tray", feature = "ayatana")))] +fn main() { + println!("This platform doesn't have the `tray` or `ayatana` feature enabled."); +} diff --git a/examples/user_agent.rs b/examples/user_agent.rs new file mode 100644 index 0000000..c5e74c4 --- /dev/null +++ b/examples/user_agent.rs @@ -0,0 +1,45 @@ +// Copyright 2019-2021 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/output.pdf b/output.pdf deleted file mode 100644 index 7436cc7..0000000 Binary files a/output.pdf and /dev/null differ diff --git a/src/shared/http/mod.rs b/src/http/mod.rs similarity index 100% rename from src/shared/http/mod.rs rename to src/http/mod.rs diff --git a/src/shared/http/request.rs b/src/http/request.rs similarity index 99% rename from src/shared/http/request.rs rename to src/http/request.rs index 13541ce..980a9ec 100644 --- a/src/shared/http/request.rs +++ b/src/http/request.rs @@ -170,6 +170,7 @@ impl Builder { /// This function will append the provided key/value as a header to the /// internal `HeaderMap` being constructed. Essentially this is equivalent /// to calling `HeaderMap::append`. + #[allow(dead_code)] // It's not needed on Linux. pub fn header(self, key: K, value: V) -> Builder where HeaderName: TryFrom, diff --git a/src/shared/http/response.rs b/src/http/response.rs similarity index 100% rename from src/shared/http/response.rs rename to src/http/response.rs diff --git a/src/lib.rs b/src/lib.rs index bc0b40d..1d81308 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,13 +49,19 @@ //! and `tray` are enabled by default. //! //! - `file-drop`: Enables [`with_file_drop_handler`] to control the behaviour when there are files -//! interacting with the window. +//! interacting with the window. Enabled by default. //! - `protocol`: Enables [`with_custom_protocol`] to define custom URL scheme for handling tasks like -//! loading assets. +//! loading assets. Enabled by default. //! - `tray`: Enables system tray and more menu item variants on **Linux**. You can still create //! those types if you disable it. They just don't create the actual objects. We set this flag //! because some implementations require more installed packages. Disable this if you don't want -//! to install `libappindicator` package. +//! to install `libappindicator` package. Enabled by default. +//! - `ayatana`: Enable this if you wish to use more update `libayatana-appindicator` since +//! `libappindicator` is no longer maintained. +//! - `transparent`: Transparent background on **macOS** requires calling private functions. +//! Disable this if you are avoiding them. +//! - `fullscreen`: Fullscreen video and other medias on **macOS** requires calling private functions. +//! Disable this if you are avoiding them. //! - `dox`: Enables this in `package.metadata.docs.rs` section to skip linking some **Linux** //! libraries and prevent from building documentation on doc.rs fails. //! @@ -89,7 +95,7 @@ use std::sync::mpsc::{RecvError, SendError}; use crate::{ application::window::BadIcon, - shared::http::{ + http::{ header::{InvalidHeaderName, InvalidHeaderValue}, method::InvalidMethod, status::InvalidStatusCode, @@ -100,11 +106,9 @@ pub use serde_json::Value; use url::ParseError; pub mod application; +pub mod http; pub mod webview; -mod shared; -pub use shared::*; - /// Convenient type alias of Result type for wry. pub type Result = std::result::Result; @@ -162,8 +166,8 @@ pub enum Error { #[error("Icon error: {0}")] Icon(#[from] BadIcon), #[cfg(target_os = "windows")] - #[error(transparent)] - WebView2Error(#[from] webview2::Error), + #[error("WebView2 error: {0}")] + WebView2Error(webview2_com::Error), #[error("Duplicate custom protocol registered: {0}")] DuplicateCustomProtocol(String), #[error("Invalid header name: {0}")] diff --git a/src/shared/mod.rs b/src/shared/mod.rs deleted file mode 100644 index b29df64..0000000 --- a/src/shared/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2019-2021 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -/// HTTP types used by wry protocol. -pub mod http; diff --git a/src/webview/mod.rs b/src/webview/mod.rs index 1aa72c4..f065bff 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -15,7 +15,7 @@ pub use web_context::WebContext; target_os = "netbsd", target_os = "openbsd" ))] -mod webkitgtk; +pub(crate) mod webkitgtk; #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -25,31 +25,35 @@ mod webkitgtk; ))] use webkitgtk::*; #[cfg(any(target_os = "macos", target_os = "ios"))] -mod wkwebview; +pub(crate) mod wkwebview; #[cfg(any(target_os = "macos", target_os = "ios"))] use wkwebview::*; #[cfg(target_os = "windows")] -mod webview2; +pub(crate) mod webview2; #[cfg(target_os = "windows")] use self::webview2::*; - -use crate::{Error, Result}; +use crate::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::{path::PathBuf, rc::Rc}; -use serde_json::Value; use url::Url; #[cfg(target_os = "windows")] use crate::application::platform::windows::WindowExtWindows; -use crate::application::window::Window; +use crate::application::{dpi::PhysicalSize, window::Window}; use crate::http::{Request as HttpRequest, Response as HttpResponse}; 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. + /// Whether the WebView should be transparent. Not supported on Windows 7. pub transparent: bool, /// Whether load the provided URL to [`WebView`]. pub url: Option, @@ -89,17 +93,11 @@ pub struct WebViewAttributes { /// /// [bug]: https://bugs.webkit.org/show_bug.cgi?id=229034 pub custom_protocols: Vec<(String, Box Result>)>, - /// Set the RPC handler to Communicate between the host Rust code and Javascript on webview. - /// - /// The communication is done via [JSON-RPC](https://www.jsonrpc.org). Users can use this to register an incoming - /// request handler and reply with responses that are passed back to Javascript. On the Javascript - /// side the client is exposed via `window.rpc` with two public methods: - /// - /// 1. The `call()` function accepts a method name and parameters and expects a reply. - /// 2. The `notify()` function accepts a method name and parameters but does not expect a reply. + /// 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 rpc_handler: Option Option>>, + pub ipc_handler: Option>, /// Set a handler closure to process incoming [`FileDropEvent`] of the webview. /// /// # Blocking OS Default Behavior @@ -111,19 +109,27 @@ pub struct WebViewAttributes { pub file_drop_handler: Option bool>>, #[cfg(not(feature = "file-drop"))] file_drop_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, } impl Default for WebViewAttributes { fn default() -> Self { Self { + user_agent: None, visible: true, transparent: false, url: None, html: None, initialization_scripts: vec![], custom_protocols: vec![], - rpc_handler: None, + ipc_handler: None, file_drop_handler: None, + clipboard: false, } } } @@ -152,7 +158,7 @@ impl<'a> WebViewBuilder<'a> { }) } - /// Sets whether the WebView should be transparent. + /// Sets whether the WebView should be transparent. Not supported on Windows 7. pub fn with_transparent(mut self, transparent: bool) -> Self { self.webview.transparent = transparent; self @@ -202,21 +208,15 @@ impl<'a> WebViewBuilder<'a> { self } - /// Set the RPC handler to Communicate between the host Rust code and Javascript on webview. - /// - /// The communication is done via [JSON-RPC](https://www.jsonrpc.org). Users can use this to register an incoming - /// request handler and reply with responses that are passed back to Javascript. On the Javascript - /// side the client is exposed via `window.rpc` with two public methods: - /// - /// 1. The `call()` function accepts a method name and parameters and expects a reply. - /// 2. The `notify()` function accepts a method name and parameters but does not expect a reply. + /// 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 fn with_rpc_handler(mut self, handler: F) -> Self + pub fn with_ipc_handler(mut self, handler: F) -> Self where - F: Fn(&Window, RpcRequest) -> Option + 'static, + F: Fn(&Window, String) + 'static, { - self.webview.rpc_handler = Some(Box::new(handler)); + self.webview.ipc_handler = Some(Box::new(handler)); self } @@ -266,6 +266,12 @@ impl<'a> WebViewBuilder<'a> { 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 + } + /// Consume the builder and create the [`WebView`]. /// /// Platform-specific behavior: @@ -274,60 +280,7 @@ impl<'a> WebViewBuilder<'a> { /// called in the same thread with the [`EventLoop`] you create. /// /// [`EventLoop`]: crate::application::event_loop::EventLoop - pub fn build(mut self) -> Result { - if self.webview.rpc_handler.is_some() { - let js = r#" - (function() { - function Rpc() { - const self = this; - this._promises = {}; - - // Private internal function called on error - this._error = (id, error) => { - if(this._promises[id]){ - this._promises[id].reject(error); - delete this._promises[id]; - } - } - - // Private internal function called on result - this._result = (id, result) => { - if(this._promises[id]){ - this._promises[id].resolve(result); - delete this._promises[id]; - } - } - - // Call remote method and expect a reply from the handler - this.call = function(method) { - let array = new Uint32Array(1); - window.crypto.getRandomValues(array); - const id = array[0]; - const params = Array.prototype.slice.call(arguments, 1); - const payload = {jsonrpc: "2.0", id, method, params}; - const promise = new Promise((resolve, reject) => { - self._promises[id] = {resolve, reject}; - }); - window.external.invoke(JSON.stringify(payload)); - return promise; - } - - // Send a notification without an `id` so no reply is expected. - this.notify = function(method) { - const params = Array.prototype.slice.call(arguments, 1); - const payload = {jsonrpc: "2.0", method, params}; - window.external.invoke(JSON.stringify(payload)); - return Promise.resolve(); - } - } - window.external = window.external || {}; - window.external.rpc = new Rpc(); - window.rpc = window.external.rpc; - })(); - "#; - - self.webview.initialization_scripts.push(js.to_string()); - } + pub fn build(self) -> Result { let window = Rc::new(self.window); let webview = InnerWebView::new(window.clone(), self.webview, self.web_context)?; Ok(WebView { window, webview }) @@ -363,15 +316,14 @@ impl Drop for WebView { } #[cfg(target_os = "windows")] unsafe { - use winapi::{shared::windef::HWND, um::winuser::DestroyWindow}; - DestroyWindow(self.window.hwnd() as HWND); + 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 rpc handler, and many more before starting WebView. To + /// 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: @@ -408,7 +360,7 @@ impl WebView { /// provide a way to resize automatically. pub fn resize(&self) -> Result<()> { #[cfg(target_os = "windows")] - self.webview.resize(self.window.hwnd())?; + self.webview.resize(HWND(self.window.hwnd() as _))?; Ok(()) } @@ -421,101 +373,15 @@ impl WebView { pub fn focus(&self) { self.webview.focus(); } -} -// Helper so all platforms handle RPC messages consistently. -fn rpc_proxy( - window: &Window, - js: String, - handler: &dyn Fn(&Window, RpcRequest) -> Option, -) -> Result> { - let req = serde_json::from_str::(&js) - .map_err(|e| Error::RpcScriptError(e.to_string(), js))?; - - let mut response = (handler)(window, req); - // Got a synchronous response so convert it to a script to be evaluated - if let Some(mut response) = response.take() { - if let Some(id) = response.id { - let js = if let Some(error) = response.error.take() { - RpcResponse::get_error_script(id, error)? - } else if let Some(result) = response.result.take() { - RpcResponse::get_result_script(id, result)? - } else { - // No error or result, assume a positive response - // with empty result (ACK) - RpcResponse::get_result_script(id, Value::Null)? - }; - Ok(Some(js)) - } else { - Ok(None) + pub fn inner_size(&self) -> PhysicalSize { + #[cfg(target_os = "macos")] + { + let scale_factor = self.window.scale_factor(); + self.webview.inner_size(scale_factor) } - } else { - Ok(None) - } -} - -const RPC_VERSION: &str = "2.0"; - -/// RPC request message. -/// -/// This usually passes to the [`RpcHandler`] or [`WindowRpcHandler`](crate::WindowRpcHandler) as -/// the parameter. You don't create this by yourself. -#[derive(Debug, Serialize, Deserialize)] -pub struct RpcRequest { - jsonrpc: String, - pub id: Option, - pub method: String, - pub params: Option, -} - -/// RPC response message which being sent back to the Javascript side. -#[derive(Debug, Serialize, Deserialize)] -pub struct RpcResponse { - jsonrpc: String, - pub(crate) id: Option, - pub(crate) result: Option, - pub(crate) error: Option, -} - -impl RpcResponse { - /// Create a new result response. - pub fn new_result(id: Option, result: Option) -> Self { - Self { - jsonrpc: RPC_VERSION.to_string(), - id, - result, - error: None, - } - } - - /// Create a new error response. - pub fn new_error(id: Option, error: Option) -> Self { - Self { - jsonrpc: RPC_VERSION.to_string(), - id, - error, - result: None, - } - } - - /// Get a script that resolves the promise with a result. - pub fn get_result_script(id: Value, result: Value) -> Result { - let retval = serde_json::to_string(&result)?; - Ok(format!( - "window.external.rpc._result({}, {})", - id.to_string(), - retval - )) - } - - /// Get a script that rejects the promise with an error. - pub fn get_error_script(id: Value, result: Value) -> Result { - let retval = serde_json::to_string(&result)?; - Ok(format!( - "window.external.rpc._error({}, {})", - id.to_string(), - retval - )) + #[cfg(not(target_os = "macos"))] + self.window.inner_size() } } @@ -540,13 +406,13 @@ pub fn webview_version() -> Result { #[cfg(target_os = "windows")] pub trait WebviewExtWindows { /// Returns WebView2 Controller - fn controller(&self) -> Option<&::webview2::Controller>; + fn controller(&self) -> Option; } #[cfg(target_os = "windows")] impl WebviewExtWindows for WebView { - fn controller(&self) -> Option<&::webview2::Controller> { - self.webview.controller.get() + fn controller(&self) -> Option { + Some(self.webview.controller.clone()) } } diff --git a/src/webview/web_context.rs b/src/webview/web_context.rs index 96bc7dd..b50dc85 100644 --- a/src/webview/web_context.rs +++ b/src/webview/web_context.rs @@ -1,3 +1,14 @@ +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use crate::webview::webkitgtk::WebContextImpl; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use crate::webview::wkwebview::WebContextImpl; + use std::path::{Path, PathBuf}; /// A context that is shared between multiple [`WebView`]s. @@ -5,12 +16,17 @@ use std::path::{Path, PathBuf}; /// A browser would have a context for all the normal tabs and a different context for all the /// private/incognito tabs. /// +/// # Warning +/// If [`Webview`] is created by a WebContext. Dropping `WebContext` will cause [`WebView`] lose +/// some actions like custom protocol on Mac. Please keep both instances when you still wish to +/// interact with them. +/// /// [`WebView`]: crate::webview::WebView #[derive(Debug)] pub struct WebContext { data: WebContextData, #[allow(dead_code)] // It's not needed on Windows and macOS. - os: WebContextImpl, + pub(crate) os: WebContextImpl, } impl WebContext { @@ -49,7 +65,7 @@ impl Default for WebContext { /// Data that all [`WebContext`] share regardless of platform. #[derive(Debug, Default)] -struct WebContextData { +pub struct WebContextData { data_directory: Option, } @@ -60,23 +76,11 @@ impl WebContextData { } } -#[cfg(not(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -)))] +#[cfg(target_os = "windows")] #[derive(Debug)] -struct WebContextImpl; +pub(crate) struct WebContextImpl; -#[cfg(not(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -)))] +#[cfg(target_os = "windows")] impl WebContextImpl { fn new(_data: &WebContextData) -> Self { Self @@ -84,359 +88,3 @@ impl WebContextImpl { fn set_allows_automation(&mut self, _flag: bool) {} } - -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -use self::unix::WebContextImpl; - -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -pub mod unix { - //! Unix platform extensions for [`WebContext`](super::WebContext). - - use crate::{ - http::{ - Request as HttpRequest, RequestBuilder as HttpRequestBuilder, Response as HttpResponse, - }, - Error, - }; - use glib::FileError; - use std::{ - collections::{HashSet, VecDeque}, - rc::Rc, - sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, - Mutex, - }, - }; - use url::Url; - //use webkit2gtk_sys::webkit_uri_request_get_http_headers; - use webkit2gtk::{ - traits::*, ApplicationInfo, LoadEvent, UserContentManager, WebContext, WebContextBuilder, - WebView, WebsiteDataManagerBuilder, - }; - - #[derive(Debug)] - pub(super) struct WebContextImpl { - context: WebContext, - manager: UserContentManager, - webview_uri_loader: Rc, - registered_protocols: HashSet, - automation: bool, - app_info: Option, - } - - impl WebContextImpl { - pub fn new(data: &super::WebContextData) -> Self { - use webkit2gtk::traits::*; - - let mut context_builder = WebContextBuilder::new(); - if let Some(data_directory) = data.data_directory() { - let data_manager = WebsiteDataManagerBuilder::new() - .local_storage_directory( - &data_directory - .join("localstorage") - .to_string_lossy() - .into_owned(), - ) - .indexeddb_directory( - &data_directory - .join("databases") - .join("indexeddb") - .to_string_lossy() - .into_owned(), - ) - .build(); - context_builder = context_builder.website_data_manager(&data_manager); - } - - let context = context_builder.build(); - - let automation = false; - context.set_automation_allowed(automation); - - // e.g. wry 0.9.4 - let app_info = ApplicationInfo::new(); - app_info.set_name(env!("CARGO_PKG_NAME")); - app_info.set_version( - env!("CARGO_PKG_VERSION_MAJOR") - .parse() - .expect("invalid wry version major"), - env!("CARGO_PKG_VERSION_MINOR") - .parse() - .expect("invalid wry version minor"), - env!("CARGO_PKG_VERSION_PATCH") - .parse() - .expect("invalid wry version patch"), - ); - - Self { - context, - automation, - manager: UserContentManager::new(), - registered_protocols: Default::default(), - webview_uri_loader: Rc::default(), - app_info: Some(app_info), - } - } - - pub fn set_allows_automation(&mut self, flag: bool) { - use webkit2gtk::traits::*; - self.automation = flag; - self.context.set_automation_allowed(flag); - } - } - - /// [`WebContext`](super::WebContext) items that only matter on unix. - pub trait WebContextExt { - /// The GTK [`WebContext`] of all webviews in the context. - fn context(&self) -> &WebContext; - - /// The GTK [`UserContentManager`] of all webviews in the context. - fn manager(&self) -> &UserContentManager; - - /// Register a custom protocol to the web context. - /// - /// When duplicate schemes are registered, the duplicate handler will still be submitted and the - /// `Err(Error::DuplicateCustomProtocol)` will be returned. It is safe to ignore if you are - /// relying on the platform's implementation to properly handle duplicated scheme handlers. - fn register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> - where - F: Fn(&HttpRequest) -> crate::Result + 'static; - - /// Register a custom protocol to the web context, only if it is not a duplicate scheme. - /// - /// If a duplicate scheme has been passed, its handler will **NOT** be registered and the - /// function will return `Err(Error::DuplicateCustomProtocol)`. - fn try_register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> - where - F: Fn(&HttpRequest) -> crate::Result + 'static; - - /// 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); - - /// Flush all queued [`WebView`]s waiting to load a uri. - /// - /// See the `WebviewUriLoader` for more information. - fn flush_queue_loader(&self); - - /// If the context allows automation. - /// - /// **Note:** `libwebkit2gtk` only allows 1 automation context at a time. - fn allows_automation(&self) -> bool; - - fn register_automation(&mut self, webview: WebView); - } - - impl WebContextExt for super::WebContext { - fn context(&self) -> &WebContext { - &self.os.context - } - - fn manager(&self) -> &UserContentManager { - &self.os.manager - } - - fn register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> - where - F: Fn(&HttpRequest) -> crate::Result + 'static, - { - actually_register_uri_scheme(self, name, handler)?; - if self.os.registered_protocols.insert(name.to_string()) { - Ok(()) - } else { - Err(Error::DuplicateCustomProtocol(name.to_string())) - } - } - - fn try_register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> - where - F: Fn(&HttpRequest) -> crate::Result + 'static, - { - if self.os.registered_protocols.insert(name.to_string()) { - actually_register_uri_scheme(self, name, handler) - } else { - Err(Error::DuplicateCustomProtocol(name.to_string())) - } - } - - fn queue_load_uri(&self, webview: Rc, url: Url) { - self.os.webview_uri_loader.push(webview, url) - } - - fn flush_queue_loader(&self) { - Rc::clone(&self.os.webview_uri_loader).flush() - } - - fn allows_automation(&self) -> bool { - self.os.automation - } - - fn register_automation(&mut self, webview: WebView) { - use webkit2gtk::traits::*; - - if let (true, Some(app_info)) = (self.os.automation, self.os.app_info.take()) { - self.os.context.connect_automation_started(move |_, auto| { - let webview = webview.clone(); - auto.set_application_info(&app_info); - - // We do **NOT** support arbitrarily creating new webviews. - // To support this in the future, we would need a way to specify the - // default WindowBuilder to use to create the window it will use, and - // possibly "default" webview attributes. Difficulty comes in for controlling - // the owned Window that would need to be used. - // - // Instead, we just pass the first created webview. - auto.connect_create_web_view(None, move |_| webview.clone()); - }); - } - } - } - - fn actually_register_uri_scheme( - context: &mut super::WebContext, - name: &str, - handler: F, - ) -> crate::Result<()> - where - F: Fn(&HttpRequest) -> crate::Result + 'static, - { - use webkit2gtk::traits::*; - let context = &context.os.context; - context - .security_manager() - .ok_or(Error::MissingManager)? - .register_uri_scheme_as_secure(name); - - context.register_uri_scheme(name, move |request| { - if let Some(uri) = request.uri() { - let uri = uri.as_str(); - - //let headers = unsafe { - // webkit_uri_request_get_http_headers(request.clone().to_glib_none().0) - //}; - - // FIXME: Read the method - // FIXME: Read the headers - // FIXME: Read the body (forms post) - let http_request = HttpRequestBuilder::new() - .uri(uri) - .method("GET") - .body(Vec::new()) - .unwrap(); - - match handler(&http_request) { - Ok(http_response) => { - let buffer = http_response.body(); - - // FIXME: Set status code - // FIXME: Set sent headers - - let input = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(buffer)); - request.finish(&input, buffer.len() as i64, http_response.mimetype()) - } - Err(_) => request.finish_error(&mut glib::Error::new( - FileError::Exist, - "Could not get requested file.", - )), - } - } else { - request.finish_error(&mut glib::Error::new( - FileError::Exist, - "Could not get uri.", - )); - } - }); - - Ok(()) - } - - /// Prevents an unknown concurrency bug with loading multiple URIs at the same time on webkit2gtk. - /// - /// Using the queue prevents data race issues with loading uris for multiple [`WebView`]s in the - /// same context at the same time. Occasionally, the one of the [`WebView`]s will be clobbered - /// and it's content will be injected into a different [`WebView`]. - /// - /// Example of `webview-c` clobbering `webview-b` while `webview-a` is okay: - /// ```text - /// webview-a triggers load-change::started - /// URISchemeRequestCallback triggered with webview-a - /// webview-a triggers load-change::committed - /// webview-a triggers load-change::finished - /// webview-b triggers load-change::started - /// webview-c triggers load-change::started - /// URISchemeRequestCallback triggered with webview-c - /// URISchemeRequestCallback triggered with webview-c - /// webview-c triggers load-change::committed - /// webview-c triggers load-change::finished - /// ``` - /// - /// In that example, `webview-a` will load fine. `webview-b` will remain empty as the uri was - /// never loaded. `webview-c` will contain the content of both `webview-b` and `webview-c` - /// because it was triggered twice even through only started once. The content injected will not - /// be sequential, and often is interjected in the middle of one of the other contents. - /// - /// FIXME: We think this may be an underlying concurrency bug in webkit2gtk as the usual ways of - /// fixing threading issues are not working. Ideally, the locks are not needed if we can understand - /// the true cause of the bug. - #[derive(Debug, Default)] - struct WebviewUriLoader { - lock: AtomicBool, - queue: Mutex, Url)>>, - } - - impl WebviewUriLoader { - /// Check if the lock is in use. - fn is_locked(&self) -> bool { - self.lock.swap(true, SeqCst) - } - - /// Unlock the lock. - fn unlock(&self) { - self.lock.store(false, SeqCst) - } - - /// Add a [`WebView`] to the queue. - fn push(&self, webview: Rc, url: Url) { - let mut queue = self.queue.lock().expect("poisoned load queue"); - queue.push_back((webview, url)) - } - - /// Remove a [`WebView`] from the queue and return it. - fn pop(&self) -> Option<(Rc, Url)> { - let mut queue = self.queue.lock().expect("poisoned load queue"); - queue.pop_front() - } - - /// Load the next uri to load if the lock is not engaged. - fn flush(self: Rc) { - if !self.is_locked() { - if let Some((webview, url)) = self.pop() { - // we do not need to listen to failed events because those will finish the change event anyways - webview.connect_load_changed(move |_, event| { - if let LoadEvent::Finished = event { - self.unlock(); - Rc::clone(&self).flush(); - }; - }); - - webview.load_uri(url.as_str()); - } else { - self.unlock(); - } - } - } - } -} diff --git a/src/webview/webkitgtk/mod.rs b/src/webview/webkitgtk/mod.rs index abc2c35..ed8482c 100644 --- a/src/webview/webkitgtk/mod.rs +++ b/src/webview/webkitgtk/mod.rs @@ -2,9 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::rc::Rc; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + rc::Rc, +}; -use gdk::{WindowEdge, RGBA}; +use gdk::{Cursor, EventMask, WindowEdge, RGBA}; use gio::Cancellable; use glib::signal::Inhibit; use gtk::prelude::*; @@ -16,20 +20,17 @@ use webkit2gtk_sys::{ webkit_get_major_version, webkit_get_micro_version, webkit_get_minor_version, }; +use web_context::WebContextExt; +pub use web_context::WebContextImpl; + use crate::{ application::{platform::unix::*, window::Window}, - webview::{ - web_context::{unix::WebContextExt, WebContext}, - WebViewAttributes, - }, + webview::{web_context::WebContext, WebViewAttributes}, Error, Result, }; -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; mod file_drop; +mod web_context; pub struct InnerWebView { webview: Rc, @@ -66,35 +67,23 @@ impl InnerWebView { // Message handler let webview = Rc::new(webview); - let wv = Rc::clone(&webview); let w = window_rc.clone(); - let rpc_handler = attributes.rpc_handler.take(); + let ipc_handler = attributes.ipc_handler.take(); + let manager = web_context.manager(); - // Use the window hash as the script handler name + // 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() }; - let manager = web_context.manager(); - // Connect before registering as recommended by the docs manager.connect_script_message_received(None, move |_m, msg| { - if let (Some(js), Some(context)) = (msg.value(), msg.global_context()) { - if let Some(js) = js.to_string(&context) { - if let Some(rpc_handler) = &rpc_handler { - match super::rpc_proxy(&w, js, rpc_handler) { - Ok(result) => { - let script = result.unwrap_or_default(); - let cancellable: Option<&Cancellable> = None; - wv.run_javascript(&script, cancellable, |_| ()); - } - Err(e) => { - eprintln!("{}", e); - } - } - } + if let Some(js) = msg.js_value() { + if let Some(ipc_handler) = &ipc_handler { + ipc_handler(&w, js.to_string()); } } }); @@ -108,6 +97,47 @@ impl InnerWebView { close_window.gtk_window().close(); }); + webview.add_events( + EventMask::POINTER_MOTION_MASK + | EventMask::BUTTON1_MOTION_MASK + | EventMask::BUTTON_PRESS_MASK + | EventMask::TOUCH_MASK, + ); + 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(), + ); + } + } + } + } + Inhibit(false) + }); webview.connect_button_press_event(|webview, event| { if event.button() == 1 { let (cx, cy) = event.root(); @@ -118,13 +148,48 @@ impl InnerWebView { // Safe to unwrap unless this is not from tao let window: gtk::Window = window.downcast().unwrap(); if !window.is_decorated() && window.is_resizable() { - // Safe to unwrap since it's a valide GtkWindow - let result = hit_test(&window.window().unwrap(), cx, cy); + if let Some(window) = window.window() { + // Safe to unwrap since it's a valide 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()), + // 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()), + } + } + } + } + } + } + Inhibit(false) + }); + 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() { + 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(), + ), + } + } } } } @@ -142,17 +207,24 @@ impl InnerWebView { } webview.grab_focus(); - // Enable webgl, webaudio, canvas features and others as default. + // Enable webgl, webaudio, canvas features as default. if let Some(settings) = WebViewExt::settings(&*webview) { settings.set_enable_webgl(true); settings.set_enable_webaudio(true); settings.set_enable_accelerated_2d_canvas(true); - settings.set_javascript_can_access_clipboard(true); + + // Enable clipboard + if attributes.clipboard { + settings.set_javascript_can_access_clipboard(true); + } // Enable App cache settings.set_enable_offline_web_application_cache(true); settings.set_enable_page_cache(true); + // Set user agent + settings.set_user_agent(attributes.user_agent.as_deref()); + debug_assert_eq!( { settings.set_enable_developer_extras(true); @@ -163,12 +235,7 @@ impl InnerWebView { // Transparent if attributes.transparent { - webview.set_background_color(&RGBA { - red: 0., - green: 0., - blue: 0., - alpha: 0., - }); + webview.set_background_color(&RGBA::new(0., 0., 0., 0.)); } // File drop handling @@ -183,10 +250,10 @@ impl InnerWebView { let w = Self { webview }; // Initialize message handler - let mut init = String::with_capacity(67 + 20 + 20); - init.push_str("window.external={invoke:function(x){window.webkit.messageHandlers[\""); + 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("\"].postMessage(x);}}"); + init.push_str("\"].postMessage(x)}})})"); w.init(&init)?; // Initialize scripts @@ -229,6 +296,9 @@ impl InnerWebView { if let Some(manager) = self.webview.user_content_manager() { let script = UserScript::new( js, + // FIXME: We allow subframe injection because webview2 does and cannot be disabled (currently). + // once webview2 allows disabling all-frame script injection, TopFrame should be set + // if it does not break anything. (originally added for isolation pattern). UserContentInjectedFrames::TopFrame, UserScriptInjectionTime::Start, &[], diff --git a/src/webview/webkitgtk/web_context.rs b/src/webview/webkitgtk/web_context.rs new file mode 100644 index 0000000..afa4d85 --- /dev/null +++ b/src/webview/webkitgtk/web_context.rs @@ -0,0 +1,337 @@ +//! Unix platform extensions for [`WebContext`](super::WebContext). + +use crate::{ + http::{Request as HttpRequest, RequestBuilder as HttpRequestBuilder, Response as HttpResponse}, + webview::web_context::WebContextData, + Error, +}; +use glib::FileError; +use std::{ + collections::{HashSet, VecDeque}, + rc::Rc, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Mutex, + }, +}; +use url::Url; +//use webkit2gtk_sys::webkit_uri_request_get_http_headers; +use webkit2gtk::{ + traits::*, ApplicationInfo, CookiePersistentStorage, LoadEvent, UserContentManager, WebContext, + WebContextBuilder, WebView, WebsiteDataManagerBuilder, +}; + +#[derive(Debug)] +pub struct WebContextImpl { + context: WebContext, + manager: UserContentManager, + webview_uri_loader: Rc, + registered_protocols: HashSet, + automation: bool, + app_info: Option, +} + +impl WebContextImpl { + pub fn new(data: &WebContextData) -> Self { + use webkit2gtk::traits::*; + + let mut context_builder = WebContextBuilder::new(); + if let Some(data_directory) = data.data_directory() { + let data_manager = WebsiteDataManagerBuilder::new() + .local_storage_directory(&data_directory.join("localstorage").to_string_lossy()) + .indexeddb_directory( + &data_directory + .join("databases") + .join("indexeddb") + .to_string_lossy(), + ) + .build(); + if let Some(cookie_manager) = data_manager.cookie_manager() { + cookie_manager.set_persistent_storage( + &data_directory.join("cookies").to_string_lossy(), + CookiePersistentStorage::Text, + ); + } + context_builder = context_builder.website_data_manager(&data_manager); + } + + let context = context_builder.build(); + + let automation = false; + context.set_automation_allowed(automation); + + // e.g. wry 0.9.4 + let app_info = ApplicationInfo::new(); + app_info.set_name(env!("CARGO_PKG_NAME")); + app_info.set_version( + env!("CARGO_PKG_VERSION_MAJOR") + .parse() + .expect("invalid wry version major"), + env!("CARGO_PKG_VERSION_MINOR") + .parse() + .expect("invalid wry version minor"), + env!("CARGO_PKG_VERSION_PATCH") + .parse() + .expect("invalid wry version patch"), + ); + + Self { + context, + automation, + manager: UserContentManager::new(), + registered_protocols: Default::default(), + webview_uri_loader: Rc::default(), + app_info: Some(app_info), + } + } + + pub fn set_allows_automation(&mut self, flag: bool) { + use webkit2gtk::traits::*; + self.automation = flag; + self.context.set_automation_allowed(flag); + } +} + +/// [`WebContext`](super::WebContext) items that only matter on unix. +pub trait WebContextExt { + /// The GTK [`WebContext`] of all webviews in the context. + fn context(&self) -> &WebContext; + + /// The GTK [`UserContentManager`] of all webviews in the context. + fn manager(&self) -> &UserContentManager; + + /// Register a custom protocol to the web context. + /// + /// When duplicate schemes are registered, the duplicate handler will still be submitted and the + /// `Err(Error::DuplicateCustomProtocol)` will be returned. It is safe to ignore if you are + /// relying on the platform's implementation to properly handle duplicated scheme handlers. + fn register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> + where + F: Fn(&HttpRequest) -> crate::Result + 'static; + + /// Register a custom protocol to the web context, only if it is not a duplicate scheme. + /// + /// If a duplicate scheme has been passed, its handler will **NOT** be registered and the + /// function will return `Err(Error::DuplicateCustomProtocol)`. + fn try_register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> + where + F: Fn(&HttpRequest) -> crate::Result + 'static; + + /// 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); + + /// Flush all queued [`WebView`]s waiting to load a uri. + /// + /// See the `WebviewUriLoader` for more information. + fn flush_queue_loader(&self); + + /// If the context allows automation. + /// + /// **Note:** `libwebkit2gtk` only allows 1 automation context at a time. + fn allows_automation(&self) -> bool; + + fn register_automation(&mut self, webview: WebView); +} + +impl WebContextExt for super::WebContext { + fn context(&self) -> &WebContext { + &self.os.context + } + + fn manager(&self) -> &UserContentManager { + &self.os.manager + } + + fn register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> + where + F: Fn(&HttpRequest) -> crate::Result + 'static, + { + actually_register_uri_scheme(self, name, handler)?; + if self.os.registered_protocols.insert(name.to_string()) { + Ok(()) + } else { + Err(Error::DuplicateCustomProtocol(name.to_string())) + } + } + + fn try_register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> + where + F: Fn(&HttpRequest) -> crate::Result + 'static, + { + if self.os.registered_protocols.insert(name.to_string()) { + actually_register_uri_scheme(self, name, handler) + } else { + Err(Error::DuplicateCustomProtocol(name.to_string())) + } + } + + fn queue_load_uri(&self, webview: Rc, url: Url) { + self.os.webview_uri_loader.push(webview, url) + } + + fn flush_queue_loader(&self) { + Rc::clone(&self.os.webview_uri_loader).flush() + } + + fn allows_automation(&self) -> bool { + self.os.automation + } + + fn register_automation(&mut self, webview: WebView) { + use webkit2gtk::traits::*; + + if let (true, Some(app_info)) = (self.os.automation, self.os.app_info.take()) { + self.os.context.connect_automation_started(move |_, auto| { + let webview = webview.clone(); + auto.set_application_info(&app_info); + + // We do **NOT** support arbitrarily creating new webviews. + // To support this in the future, we would need a way to specify the + // default WindowBuilder to use to create the window it will use, and + // possibly "default" webview attributes. Difficulty comes in for controlling + // the owned Window that would need to be used. + // + // Instead, we just pass the first created webview. + auto.connect_create_web_view(None, move |_| webview.clone()); + }); + } + } +} + +fn actually_register_uri_scheme( + context: &mut super::WebContext, + name: &str, + handler: F, +) -> crate::Result<()> +where + F: Fn(&HttpRequest) -> crate::Result + 'static, +{ + use webkit2gtk::traits::*; + let context = &context.os.context; + // Enable secure context + context + .security_manager() + .ok_or(Error::MissingManager)? + .register_uri_scheme_as_secure(name); + + context.register_uri_scheme(name, move |request| { + if let Some(uri) = request.uri() { + let uri = uri.as_str(); + + //let headers = unsafe { + // webkit_uri_request_get_http_headers(request.clone().to_glib_none().0) + //}; + + // FIXME: Read the method + // FIXME: Read the headers + // FIXME: Read the body (forms post) + let http_request = HttpRequestBuilder::new() + .uri(uri) + .method("GET") + .body(Vec::new()) + .unwrap(); + + match handler(&http_request) { + Ok(http_response) => { + let buffer = http_response.body(); + + // FIXME: Set status code + // FIXME: Set sent headers + + let input = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(buffer)); + request.finish(&input, buffer.len() as i64, http_response.mimetype()) + } + Err(_) => request.finish_error(&mut glib::Error::new( + FileError::Exist, + "Could not get requested file.", + )), + } + } else { + request.finish_error(&mut glib::Error::new( + FileError::Exist, + "Could not get uri.", + )); + } + }); + + Ok(()) +} + +/// Prevents an unknown concurrency bug with loading multiple URIs at the same time on webkit2gtk. +/// +/// Using the queue prevents data race issues with loading uris for multiple [`WebView`]s in the +/// same context at the same time. Occasionally, the one of the [`WebView`]s will be clobbered +/// and it's content will be injected into a different [`WebView`]. +/// +/// Example of `webview-c` clobbering `webview-b` while `webview-a` is okay: +/// ```text +/// webview-a triggers load-change::started +/// URISchemeRequestCallback triggered with webview-a +/// webview-a triggers load-change::committed +/// webview-a triggers load-change::finished +/// webview-b triggers load-change::started +/// webview-c triggers load-change::started +/// URISchemeRequestCallback triggered with webview-c +/// URISchemeRequestCallback triggered with webview-c +/// webview-c triggers load-change::committed +/// webview-c triggers load-change::finished +/// ``` +/// +/// In that example, `webview-a` will load fine. `webview-b` will remain empty as the uri was +/// never loaded. `webview-c` will contain the content of both `webview-b` and `webview-c` +/// because it was triggered twice even through only started once. The content injected will not +/// be sequential, and often is interjected in the middle of one of the other contents. +/// +/// FIXME: We think this may be an underlying concurrency bug in webkit2gtk as the usual ways of +/// fixing threading issues are not working. Ideally, the locks are not needed if we can understand +/// the true cause of the bug. +#[derive(Debug, Default)] +struct WebviewUriLoader { + lock: AtomicBool, + queue: Mutex, Url)>>, +} + +impl WebviewUriLoader { + /// Check if the lock is in use. + fn is_locked(&self) -> bool { + self.lock.swap(true, SeqCst) + } + + /// Unlock the lock. + fn unlock(&self) { + self.lock.store(false, SeqCst) + } + + /// Add a [`WebView`] to the queue. + fn push(&self, webview: Rc, url: Url) { + let mut queue = self.queue.lock().expect("poisoned load queue"); + queue.push_back((webview, url)) + } + + /// Remove a [`WebView`] from the queue and return it. + fn pop(&self) -> Option<(Rc, Url)> { + let mut queue = self.queue.lock().expect("poisoned load queue"); + queue.pop_front() + } + + /// Load the next uri to load if the lock is not engaged. + fn flush(self: Rc) { + if !self.is_locked() { + if let Some((webview, url)) = self.pop() { + // we do not need to listen to failed events because those will finish the change event anyways + webview.connect_load_changed(move |_, event| { + if let LoadEvent::Finished = event { + self.unlock(); + Rc::clone(&self).flush(); + }; + }); + + webview.load_uri(url.as_str()); + } else { + self.unlock(); + } + } + } +} diff --git a/src/webview/webview2/file_drop.rs b/src/webview/webview2/file_drop.rs index 290c155..1440938 100644 --- a/src/webview/webview2/file_drop.rs +++ b/src/webview/webview2/file_drop.rs @@ -15,27 +15,32 @@ use std::{ path::PathBuf, ptr, rc::Rc, - sync::atomic::{AtomicUsize, Ordering}, }; -use winapi::shared::windef::HWND; +use windows::{ + self as Windows, + Win32::{ + Foundation::{self as win32f, BOOL, DRAGDROP_E_INVALIDHWND, HWND, LPARAM, POINTL, PWSTR}, + System::{ + Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}, + Ole::{IDropTarget, RegisterDragDrop, RevokeDragDrop, DROPEFFECT_COPY, DROPEFFECT_NONE}, + SystemServices::CF_HDROP, + }, + UI::{ + Shell::{DragFinish, DragQueryFileW, HDROP}, + WindowsAndMessaging::EnumChildWindows, + }, + }, +}; + +use windows_macros::implement; use crate::application::window::Window; pub(crate) struct FileDropController { - drop_targets: Vec<*mut IDropTarget>, -} -impl Drop for FileDropController { - fn drop(&mut self) { - // Safety: this could dereference a null ptr. - // This should never be a null ptr unless something goes wrong in Windows. - unsafe { - for ptr in &self.drop_targets { - Box::from_raw(*ptr); - } - } - } + drop_targets: Vec, } + impl FileDropController { pub(crate) fn new() -> Self { FileDropController { @@ -65,17 +70,13 @@ impl FileDropController { ) -> bool { // Safety: WinAPI calls are unsafe unsafe { - let file_drop_handler = IDropTarget::new(hwnd, window, listener); - let handler_interface_ptr = - &mut (*file_drop_handler.data).interface as winapi::um::oleidl::LPDROPTARGET; + let file_drop_handler: IDropTarget = FileDropHandler::new(window, listener).into(); - if winapi::um::ole2::RevokeDragDrop(hwnd) != winapi::shared::winerror::DRAGDROP_E_INVALIDHWND - && winapi::um::ole2::RegisterDragDrop(hwnd, handler_interface_ptr) == S_OK + if RevokeDragDrop(hwnd) != Err(DRAGDROP_E_INVALIDHWND.into()) + && RegisterDragDrop(hwnd, file_drop_handler.clone()).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(Box::into_raw(Box::new(file_drop_handler))); + self.drop_targets.push(file_drop_handler); } } @@ -92,255 +93,148 @@ where { 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 = closure_pointer_pointer as winapi::shared::minwindef::LPARAM; - unsafe { winapi::um::winuser::EnumChildWindows(hwnd, Some(enumerate_callback), lparam) }; + let lparam = LPARAM(closure_pointer_pointer as _); + unsafe { EnumChildWindows(hwnd, Some(enumerate_callback), lparam) }; } -unsafe extern "system" fn enumerate_callback( - hwnd: HWND, - lparam: winapi::shared::minwindef::LPARAM, -) -> winapi::shared::minwindef::BOOL { - let closure = &mut *(lparam as *mut c_void as *mut &mut dyn FnMut(HWND) -> bool); - if closure(hwnd) { - winapi::shared::minwindef::TRUE - } else { - winapi::shared::minwindef::FALSE - } +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() } -// The below code has been ripped from tao - if only they'd `pub use` this! -// https://github.com/rust-windowing/winit/blob/b9f3d333e41464457f6e42640793bf88b9563727/src/platform_impl/windows/drop_handler.rs -// Safety: WinAPI calls are unsafe - -use winapi::{ - shared::{ - guiddef::REFIID, - minwindef::{DWORD, UINT, ULONG}, - windef::POINTL, - winerror::S_OK, - }, - um::{ - objidl::IDataObject, - oleidl::{IDropTarget as NativeIDropTarget, IDropTargetVtbl, DROPEFFECT_COPY, DROPEFFECT_NONE}, - shellapi, unknwnbase, - winnt::HRESULT, - }, -}; - -#[allow(non_camel_case_types)] -#[repr(C)] -struct IDropTargetData { - pub interface: NativeIDropTarget, - listener: Rc bool>, - refcount: AtomicUsize, - hwnd: HWND, +#[implement(Windows::Win32::System::Ole::IDropTarget)] +pub struct FileDropHandler { window: Rc, - cursor_effect: DWORD, + listener: Rc bool>, + cursor_effect: u32, hovered_is_valid: bool, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */ } -#[allow(non_camel_case_types)] -pub struct IDropTarget { - data: *mut IDropTargetData, -} - #[allow(non_snake_case)] -impl IDropTarget { - fn new( - hwnd: HWND, +impl FileDropHandler { + pub fn new( window: Rc, listener: Rc bool>, - ) -> IDropTarget { - let data = Box::new(IDropTargetData { - listener, - interface: NativeIDropTarget { - lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl, - }, - refcount: AtomicUsize::new(1), - hwnd, + ) -> FileDropHandler { + Self { window, + listener, cursor_effect: DROPEFFECT_NONE, hovered_is_valid: false, - }); - IDropTarget { - data: Box::into_raw(data), } } - // Implement IUnknown - pub unsafe extern "system" fn QueryInterface( - _this: *mut unknwnbase::IUnknown, - _riid: REFIID, - _ppvObject: *mut *mut winapi::ctypes::c_void, - ) -> HRESULT { - // This function doesn't appear to be required for an `IDropTarget`. - // An implementation would be nice however. - unimplemented!(); - } - - pub unsafe extern "system" fn AddRef(this: *mut unknwnbase::IUnknown) -> ULONG { - let drop_handler_data = Self::from_interface(this); - let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; - count as ULONG - } - - pub unsafe extern "system" fn Release(this: *mut unknwnbase::IUnknown) -> ULONG { - let drop_handler = Self::from_interface(this); - let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; - if count == 0 { - // Destroy the underlying data - Box::from_raw(drop_handler as *mut IDropTargetData); - } - count as ULONG - } - - pub unsafe extern "system" fn DragEnter( - this: *mut NativeIDropTarget, - pDataObj: *const IDataObject, - _grfKeyState: DWORD, - _pt: *const POINTL, - pdwEffect: *mut DWORD, - ) -> HRESULT { + unsafe fn DragEnter( + &mut self, + pDataObj: &Option, + _grfKeyState: u32, + _pt: POINTL, + pdwEffect: *mut u32, + ) -> windows::core::Result<()> { let mut paths = Vec::new(); - - let drop_handler = Self::from_interface(this); let hdrop = Self::collect_paths(pDataObj, &mut paths); - drop_handler.hovered_is_valid = hdrop.is_some(); - drop_handler.cursor_effect = if drop_handler.hovered_is_valid { + self.hovered_is_valid = hdrop.is_some(); + self.cursor_effect = if self.hovered_is_valid { DROPEFFECT_COPY } else { DROPEFFECT_NONE }; - *pdwEffect = drop_handler.cursor_effect; + *pdwEffect = self.cursor_effect; - (drop_handler.listener)(&drop_handler.window, FileDropEvent::Hovered(paths)); + (self.listener)(&self.window, FileDropEvent::Hovered(paths)); - S_OK + Ok(()) } - pub unsafe extern "system" fn DragOver( - this: *mut NativeIDropTarget, - _grfKeyState: DWORD, - _pt: *const POINTL, - pdwEffect: *mut DWORD, - ) -> HRESULT { - let drop_handler = Self::from_interface(this); - *pdwEffect = drop_handler.cursor_effect; - - S_OK + unsafe fn DragOver( + &self, + _grfKeyState: u32, + _pt: POINTL, + pdwEffect: *mut u32, + ) -> windows::core::Result<()> { + *pdwEffect = self.cursor_effect; + Ok(()) } - pub unsafe extern "system" fn DragLeave(this: *mut NativeIDropTarget) -> HRESULT { - let drop_handler = Self::from_interface(this); - if drop_handler.hovered_is_valid { - (drop_handler.listener)(&drop_handler.window, FileDropEvent::Cancelled); + unsafe fn DragLeave(&self) -> windows::core::Result<()> { + if self.hovered_is_valid { + (self.listener)(&self.window, FileDropEvent::Cancelled); } - - S_OK + Ok(()) } - pub unsafe extern "system" fn Drop( - this: *mut NativeIDropTarget, - pDataObj: *const IDataObject, - _grfKeyState: DWORD, - _pt: *const POINTL, - _pdwEffect: *mut DWORD, - ) -> HRESULT { + unsafe fn Drop( + &self, + pDataObj: &Option, + _grfKeyState: u32, + _pt: POINTL, + _pdwEffect: *mut u32, + ) -> windows::core::Result<()> { let mut paths = Vec::new(); - - let drop_handler = Self::from_interface(this); let hdrop = Self::collect_paths(pDataObj, &mut paths); if let Some(hdrop) = hdrop { - shellapi::DragFinish(hdrop); + DragFinish(hdrop); } - (drop_handler.listener)(&drop_handler.window, FileDropEvent::Dropped(paths)); + (self.listener)(&self.window, FileDropEvent::Dropped(paths)); - S_OK - } - - unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut IDropTargetData { - &mut *(this as *mut _) + Ok(()) } unsafe fn collect_paths( - data_obj: *const IDataObject, + data_obj: &Option, paths: &mut Vec, - ) -> Option { - use winapi::{ - shared::{ - winerror::{DV_E_FORMATETC, SUCCEEDED}, - wtypes::{CLIPFORMAT, DVASPECT_CONTENT}, - }, - um::{ - objidl::{FORMATETC, TYMED_HGLOBAL}, - shellapi::DragQueryFileW, - winuser::CF_HDROP, - }, - }; - + ) -> Option { let drop_format = FORMATETC { - cfFormat: CF_HDROP as CLIPFORMAT, - ptd: ptr::null(), - dwAspect: DVASPECT_CONTENT, + cfFormat: CF_HDROP as u16, + ptd: ptr::null_mut(), + dwAspect: DVASPECT_CONTENT as u32, lindex: -1, - tymed: TYMED_HGLOBAL, + tymed: TYMED_HGLOBAL as u32, }; - let mut medium = std::mem::zeroed(); - let get_data_result = (*data_obj).GetData(&drop_format, &mut medium); - if SUCCEEDED(get_data_result) { - let hglobal = (*medium.u).hGlobal(); - let hdrop = (*hglobal) as shellapi::HDROP; + match data_obj + .as_ref() + .expect("Received null IDataObject") + .GetData(&drop_format) + { + Ok(medium) => { + let hdrop = HDROP(medium.Anonymous.hGlobal); - // The second parameter (0xFFFFFFFF) instructs the function to return the item count - let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, ptr::null_mut(), 0); + // The second parameter (0xFFFFFFFF) instructs the function to return the item count + let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, PWSTR::default(), 0); - for i in 0..item_count { - // Get the length of the path string NOT including the terminating null character. - // Previously, this was using a fixed size array of MAX_PATH length, but the - // Windows API allows longer paths under certain circumstances. - let character_count = DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize; - let str_len = character_count + 1; + for i in 0..item_count { + // Get the length of the path string NOT including the terminating null character. + // Previously, this was using a fixed size array of MAX_PATH length, but the + // Windows API allows longer paths under certain circumstances. + let character_count = DragQueryFileW(hdrop, i, PWSTR::default(), 0) as usize; + let str_len = character_count + 1; - // Fill path_buf with the null-terminated file name - let mut path_buf = Vec::with_capacity(str_len); - DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as UINT); - path_buf.set_len(str_len); + // Fill path_buf with the null-terminated file name + let mut path_buf = Vec::with_capacity(str_len); + DragQueryFileW(hdrop, i, PWSTR(path_buf.as_mut_ptr()), str_len as u32); + path_buf.set_len(str_len); - paths.push(OsString::from_wide(&path_buf[0..character_count]).into()); + paths.push(OsString::from_wide(&path_buf[0..character_count]).into()); + } + + Some(hdrop) + } + Err(error) => { + log::warn!( + "{}", + match error.code() { + win32f::DV_E_FORMATETC => { + // If the dropped item is not a file this error will occur. + // In this case it is OK to return without taking further action. + "Error occured while processing dropped/hovered item: item is not a file." + } + _ => "Unexpected error occured while processing dropped/hovered item.", + } + ); + None } - - Some(hdrop) - } else if get_data_result == DV_E_FORMATETC { - // If the dropped item is not a file this error will occur. - // In this case it is OK to return without taking further action. - log::warn!("Error occured while processing dropped/hovered item: item is not a file."); - None - } else { - log::warn!("Unexpected error occured while processing dropped/hovered item."); - None } } } - -impl Drop for IDropTarget { - fn drop(&mut self) { - unsafe { - IDropTarget::Release(self.data as *mut unknwnbase::IUnknown); - } - } -} - -static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { - parent: unknwnbase::IUnknownVtbl { - QueryInterface: IDropTarget::QueryInterface, - AddRef: IDropTarget::AddRef, - Release: IDropTarget::Release, - }, - DragEnter: IDropTarget::DragEnter, - DragOver: IDropTarget::DragOver, - DragLeave: IDropTarget::DragLeave, - Drop: IDropTarget::Drop, -}; diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs index 3441a4f..793eca6 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview/webview2/mod.rs @@ -5,34 +5,46 @@ mod file_drop; use crate::{ - application::platform::windows::EventLoopExtWindows, webview::{WebContext, WebViewAttributes}, - Result, + Error, Result, }; use file_drop::FileDropController; -use std::{collections::HashSet, io::Read, os::raw::c_void, rc::Rc}; +use std::{collections::HashSet, rc::Rc, sync::mpsc}; use once_cell::unsync::OnceCell; -use webview2::{Controller, PermissionKind, PermissionState, WebView}; -use winapi::{ - shared::{windef::HWND, winerror::E_FAIL}, - um::winuser::{DestroyWindow, GetClientRect}, + +use windows::{ + core::Interface, + Win32::{ + Foundation::{BOOL, E_FAIL, E_POINTER, HWND, POINT, PWSTR, RECT}, + System::{ + Com::{IStream, StructuredStorage::CreateStreamOnHGlobal}, + WinRT::EventRegistrationToken, + }, + UI::WindowsAndMessaging::{ + self as win32wm, DestroyWindow, GetClientRect, GetCursorPos, WM_NCLBUTTONDOWN, + }, + }, }; +use webview2_com::{Microsoft::Web::WebView2::Win32::*, *}; + use crate::{ - application::{ - event_loop::{ControlFlow, EventLoop}, - platform::{run_return::EventLoopExtRunReturn, windows::WindowExtWindows}, - window::Window, - }, + application::{platform::windows::WindowExtWindows, window::Window}, http::RequestBuilder as HttpRequestBuilder, }; +impl From for Error { + fn from(err: webview2_com::Error) -> Self { + Error::WebView2Error(err) + } +} + pub struct InnerWebView { - pub(crate) controller: Rc>, - webview: Rc>, + pub(crate) controller: ICoreWebView2Controller, + webview: ICoreWebView2, // 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)] @@ -45,304 +57,21 @@ impl InnerWebView { mut attributes: WebViewAttributes, web_context: Option<&mut WebContext>, ) -> Result { - let hwnd = window.hwnd() as HWND; - - let controller: Rc> = Rc::new(OnceCell::new()); - let controller_clone = controller.clone(); - + let hwnd = HWND(window.hwnd() as _); let file_drop_controller: Rc> = Rc::new(OnceCell::new()); - let file_drop_controller_clone = file_drop_controller.clone(); + let file_drop_handler = attributes.file_drop_handler.take(); + let file_drop_window = window.clone(); - let mut webview_builder = webview2::EnvironmentBuilder::new(); + let env = Self::create_environment(&web_context)?; + let controller = Self::create_controller(hwnd, &env)?; + let webview = Self::init_webview(window, hwnd, attributes, &env, &controller)?; - if let Some(web_context) = web_context { - if let Some(data_directory) = web_context.data_directory() { - webview_builder = webview_builder.with_user_data_folder(&data_directory); - } + 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); } - // Webview controller - webview_builder.build(move |env| { - let env = env?; - let env_ = env.clone(); - env.create_controller(hwnd, move |controller| { - let controller = controller?; - let w = controller.get_webview()?; - - w.add_window_close_requested(move |_| { - if unsafe { DestroyWindow(hwnd as HWND) } != 0 { - Ok(()) - } else { - Err(webview2::Error::new(E_FAIL)) - } - })?; - - // Transparent - if attributes.transparent { - if let Ok(c2) = controller.get_controller2() { - c2.put_default_background_color(webview2_sys::Color { - r: 0, - g: 0, - b: 0, - a: 0, - })?; - } - } - - // Enable sensible defaults - let settings = w.get_settings()?; - settings.put_is_status_bar_enabled(false)?; - settings.put_are_default_context_menus_enabled(true)?; - settings.put_is_zoom_control_enabled(false)?; - settings.put_are_dev_tools_enabled(false)?; - debug_assert_eq!(settings.put_are_dev_tools_enabled(true)?, ()); - - // Safety: System calls are unsafe - unsafe { - let mut rect = std::mem::zeroed(); - GetClientRect(hwnd, &mut rect); - controller.put_bounds(rect)?; - } - - // Initialize scripts - w.add_script_to_execute_on_document_created( - r#" - window.external={invoke: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', () => window.chrome.webview.postMessage('__WEBVIEW_MOUSE_MOVE__')); - "#, - |_| (Ok(())), - )?; - for js in attributes.initialization_scripts { - w.add_script_to_execute_on_document_created(&js, |_| (Ok(())))?; - } - - // Message handler - let window_ = window.clone(); - - let rpc_handler = attributes.rpc_handler.take(); - w.add_web_message_received(move |webview, args| { - let js = args.try_get_web_message_as_string()?; - if js == "__WEBVIEW_LEFT_MOUSE_DOWN__" || js == "__WEBVIEW_MOUSE_MOVE__" { - if !window_.is_decorated() && window_.is_resizable() { - use winapi::um::winuser::{ - HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTLEFT, HTRIGHT, - HTTOP, HTTOPLEFT, HTTOPRIGHT, HTCLIENT, GetCursorPos, - }; - use crate::application::{window::CursorIcon,platform::windows::hit_test}; - - let (cx, cy); - unsafe { - let mut point = std::mem::zeroed(); - GetCursorPos(&mut point); - cx = point.x; - cy = point.y; - }; - let result = hit_test(window_.hwnd() as _, cx, cy); - let cursor = match result { - HTLEFT => CursorIcon::WResize, - HTTOP => CursorIcon::NResize, - HTRIGHT => CursorIcon::EResize, - HTBOTTOM => CursorIcon::SResize, - HTTOPLEFT => CursorIcon::NwResize, - HTTOPRIGHT => CursorIcon::NeResize, - HTBOTTOMLEFT => CursorIcon::SwResize, - 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 != HTCLIENT { - window_.begin_resize_drag(result); - } - } - } - // these are internal messages, rpc_handlers don't need it so exit early - return Ok(()); - } - - if let Some(rpc_handler) = &rpc_handler { - match super::rpc_proxy(&window_, js, rpc_handler) { - Ok(result) => { - if let Some(ref script) = result { - webview.execute_script(script, |_| (Ok(())))?; - } - } - Err(e) => { - eprintln!("{}", e); - } - } - } - Ok(()) - })?; - - let mut custom_protocol_names = HashSet::new(); - if !attributes.custom_protocols.is_empty() { - for (name, _) in &attributes.custom_protocols { - // WebView2 doesn't support non-standard protocols yet, so we have to use this workaround - // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 - custom_protocol_names.insert(name.clone()); - w.add_web_resource_requested_filter( - &format!("https://{}.*", name), - webview2::WebResourceContext::All, - )?; - } - - let custom_protocols = attributes.custom_protocols; - let env_clone = env_.clone(); - w.add_web_resource_requested(move |_, args| { - let webview_request = args.get_request()?; - let mut request = HttpRequestBuilder::new(); - - // request method (GET, POST, PUT etc..) - let request_method = webview_request.get_method()?; - - // get all headers from the request - let headers = webview_request.get_headers()?; - for (key, value) in headers.get_iterator()? { - request = request.header(&key, &value); - } - - // get the body content if available - let mut body_sent = Vec::new(); - if let Ok(mut content) = webview_request.get_content() { - content.read_to_end(&mut body_sent)?; - } - - // uri - let uri = webview_request.get_uri()?; - - // Undo the protocol workaround when giving path to resolver - let path = uri - .replace("https://", "") - .replacen(".", "://", 1); - - let scheme = path.split("://").next().unwrap(); - - let final_request = request.uri(&path).method(request_method.as_str()).body(body_sent).unwrap(); - match (custom_protocols - .iter() - .find(|(name, _)| name == &scheme) - .unwrap() - .1)(&final_request) - { - Ok(sent_response) => { - - let mime = sent_response.mimetype(); - let content = sent_response.body(); - let status_code = sent_response.status().as_u16() as i32; - - let mut headers_map = String::new(); - - // set mime type if provided - if let Some(mime) = sent_response.mimetype() { - headers_map.push_str(&format!("Content-Type: {}\n", mime)) - } - - // build headers - for (name, value) in sent_response.headers().iter() { - let header_key = name.to_string(); - if let Ok(value) = value.to_str() { - headers_map.push_str(&format!("{}: {}\n", header_key, value)) - } - } - - let stream = webview2::Stream::from_bytes(&content); - - // FIXME: Set http response version - - let response = env_clone.create_web_resource_response( - stream, - status_code, - "OK", - &headers_map, - )?; - - args.put_response(response)?; - Ok(()) - } - Err(_) => Err(webview2::Error::from(std::io::Error::new( - std::io::ErrorKind::Other, - "Error loading requested file", - ))), - } - })?; - } - - // Enable clipboard - w.add_permission_requested(|_, args| { - let kind = args.get_permission_kind()?; - if kind == PermissionKind::ClipboardRead { - args.put_state(PermissionState::Allow)?; - } - Ok(()) - })?; - - // Navigation - if let Some(url) = attributes.url { - if url.cannot_be_a_base() { - let s = url.as_str(); - if let Some(pos) = s.find(',') { - let (_, path) = s.split_at(pos + 1); - w.navigate_to_string(path)?; - } - } else { - let mut url_string = String::from(url.as_str()); - let name = url.scheme(); - if custom_protocol_names.contains(name) { - // WebView2 doesn't support non-standard protocols yet, so we have to use this workaround - // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 - url_string = url.as_str().replace( - &format!("{}://", name), - &format!("https://{}.", name), - ) - } - w.navigate(&url_string)?; - } - } else if let Some(html) = attributes.html { - w.navigate_to_string(&html)?; - } - - controller.put_is_visible(true)?; - controller.move_focus(webview2::MoveFocusReason::Programmatic)?; - let _ = controller_clone.set(controller); - - if let Some(file_drop_handler) = attributes.file_drop_handler { - let mut file_drop_controller = FileDropController::new(); - file_drop_controller.listen(hwnd, window.clone(), file_drop_handler); - let _ = file_drop_controller_clone.set(file_drop_controller); - } - - Ok(()) - }) - })?; - - // Wait until webview is actually created - let mut event_loop: EventLoop<()> = EventLoop::new_any_thread(); - let controller_clone = controller.clone(); - let webview: Rc> = Rc::new(OnceCell::new()); - let webview_clone = webview.clone(); - event_loop.run_return(|_, _, control_flow| { - if let Some(c) = controller_clone.get() { - if let Ok(wv) = c.get_webview() { - *control_flow = ControlFlow::Exit; - let _ = webview_clone.set(wv); - } else { - *control_flow = ControlFlow::Poll; - } - } - }); - // TODO: OnceCell into_inner for controller - Ok(Self { controller, webview, @@ -350,43 +79,510 @@ impl InnerWebView { }) } + fn create_environment( + web_context: &Option<&mut WebContext>, + ) -> webview2_com::Result { + let (tx, rx) = mpsc::channel(); + + let data_directory = web_context + .as_deref() + .and_then(|context| context.data_directory()) + .and_then(|path| path.to_str()) + .map(String::from); + + CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async_operation( + Box::new(move |environmentcreatedhandler| unsafe { + if let Some(data_directory) = data_directory { + // If we have a custom data_directory, we need to use a call to `CreateCoreWebView2EnvironmentWithOptions` + // instead of the much simpler `CreateCoreWebView2Environment`. + let options: ICoreWebView2EnvironmentOptions = + CoreWebView2EnvironmentOptions::default().into(); + let data_directory = pwstr_from_str(&data_directory); + let result = CreateCoreWebView2EnvironmentWithOptions( + PWSTR::default(), + data_directory, + options, + environmentcreatedhandler, + ) + .map_err(webview2_com::Error::WindowsError); + let _ = take_pwstr(data_directory); + + return result; + } + + CreateCoreWebView2Environment(environmentcreatedhandler) + .map_err(webview2_com::Error::WindowsError) + }), + Box::new(move |error_code, environment| { + error_code?; + tx.send(environment.ok_or_else(|| windows::core::Error::fast_error(E_POINTER))) + .expect("send over mpsc channel"); + Ok(()) + }), + )?; + + rx.recv() + .map_err(|_| webview2_com::Error::SendError)? + .map_err(webview2_com::Error::WindowsError) + } + + fn create_controller( + hwnd: HWND, + env: &ICoreWebView2Environment, + ) -> webview2_com::Result { + let (tx, rx) = mpsc::channel(); + let env = env.clone(); + + CreateCoreWebView2ControllerCompletedHandler::wait_for_async_operation( + Box::new(move |handler| unsafe { + env + .CreateCoreWebView2Controller(hwnd, handler) + .map_err(webview2_com::Error::WindowsError) + }), + Box::new(move |error_code, controller| { + error_code?; + tx.send(controller.ok_or_else(|| windows::core::Error::fast_error(E_POINTER))) + .expect("send over mpsc channel"); + Ok(()) + }), + )?; + + rx.recv() + .map_err(|_| webview2_com::Error::SendError)? + .map_err(webview2_com::Error::WindowsError) + } + + fn init_webview( + window: Rc, + hwnd: HWND, + mut attributes: WebViewAttributes, + env: &ICoreWebView2Environment, + controller: &ICoreWebView2Controller, + ) -> webview2_com::Result { + let webview = + unsafe { controller.CoreWebView2() }.map_err(webview2_com::Error::WindowsError)?; + + // Transparent + if attributes.transparent && !is_windows_7() { + let controller2: ICoreWebView2Controller2 = controller + .cast() + .map_err(webview2_com::Error::WindowsError)?; + unsafe { + controller2 + .SetDefaultBackgroundColor(COREWEBVIEW2_COLOR { + R: 0, + G: 0, + B: 0, + A: 0, + }) + .map_err(webview2_com::Error::WindowsError)?; + } + } + + // The EventRegistrationToken is an out-param from all of the event registration calls. We're + // taking it in the local variable and then just ignoring it because all of the event handlers + // are registered for the life of the webview, but if we wanted to be able to remove them later + // we would hold onto them in self. + let mut token = EventRegistrationToken::default(); + + // Safety: System calls are unsafe + unsafe { + let handler: ICoreWebView2WindowCloseRequestedEventHandler = + WindowCloseRequestedEventHandler::create(Box::new(move |_, _| { + if DestroyWindow(hwnd).as_bool() { + Ok(()) + } else { + Err(E_FAIL.into()) + } + })); + webview + .WindowCloseRequested(handler, &mut token) + .map_err(webview2_com::Error::WindowsError)?; + + let settings = webview + .Settings() + .map_err(webview2_com::Error::WindowsError)?; + settings + .SetIsStatusBarEnabled(false) + .map_err(webview2_com::Error::WindowsError)?; + settings + .SetAreDefaultContextMenusEnabled(true) + .map_err(webview2_com::Error::WindowsError)?; + settings + .SetIsZoomControlEnabled(false) + .map_err(webview2_com::Error::WindowsError)?; + settings + .SetAreDevToolsEnabled(false) + .map_err(webview2_com::Error::WindowsError)?; + debug_assert_eq!(settings.SetAreDevToolsEnabled(true), Ok(())); + + let mut rect = RECT::default(); + GetClientRect(hwnd, &mut rect); + controller + .SetBounds(rect) + .map_err(webview2_com::Error::WindowsError)?; + } + + // Initialize scripts + 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__'));"#, + ), + )?; + for js in attributes.initialization_scripts { + Self::add_script_to_execute_on_document_created(&webview, js)?; + } + + // Message handler + let ipc_handler = attributes.ipc_handler.take(); + unsafe { + webview.WebMessageReceived( + WebMessageReceivedEventHandler::create(Box::new(move |_, args| { + if let Some(args) = args { + let mut js = PWSTR::default(); + 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() { + use crate::application::{platform::windows::hit_test, window::CursorIcon}; + + let mut point = POINT::default(); + GetCursorPos(&mut point); + let result = hit_test(HWND(window.hwnd() as _), 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 { + window.begin_resize_drag(result.0, 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); + } + } + + Ok(()) + })), + &mut token, + ) + } + .map_err(webview2_com::Error::WindowsError)?; + + let mut custom_protocol_names = HashSet::new(); + if !attributes.custom_protocols.is_empty() { + for (name, _) in &attributes.custom_protocols { + // WebView2 doesn't support non-standard protocols yet, so we have to use this workaround + // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 + custom_protocol_names.insert(name.clone()); + unsafe { + webview.AddWebResourceRequestedFilter( + format!("https://{}.*", name), + COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL, + ) + } + .map_err(webview2_com::Error::WindowsError)?; + } + + let custom_protocols = attributes.custom_protocols; + let env = env.clone(); + unsafe { + webview + .WebResourceRequested( + WebResourceRequestedEventHandler::create(Box::new(move |_, args| { + if let Some(args) = args { + let webview_request = args.Request()?; + let mut request = HttpRequestBuilder::new(); + + // request method (GET, POST, PUT etc..) + let mut request_method = PWSTR::default(); + webview_request.Method(&mut request_method)?; + let request_method = take_pwstr(request_method); + + // get all headers from the request + let headers = webview_request.Headers()?.GetIterator()?; + let mut has_current = BOOL::default(); + headers.HasCurrentHeader(&mut has_current)?; + if has_current.as_bool() { + loop { + let mut key = PWSTR::default(); + let mut value = PWSTR::default(); + headers.GetCurrentHeader(&mut key, &mut value)?; + let (key, value) = (take_pwstr(key), take_pwstr(value)); + request = request.header(&key, &value); + + headers.MoveNext(&mut has_current)?; + if !has_current.as_bool() { + break; + } + } + } + + // get the body content if available + let mut body_sent = Vec::new(); + if let Ok(content) = webview_request.Content() { + let mut buffer: [u8; 1024] = [0; 1024]; + loop { + let mut cb_read = 0; + let content: IStream = content.cast()?; + content.Read( + buffer.as_mut_ptr() as *mut _, + buffer.len() as u32, + &mut cb_read, + )?; + + if cb_read == 0 { + break; + } + + body_sent.extend_from_slice(&buffer[..(cb_read as usize)]); + } + } + + // uri + let mut uri = PWSTR::default(); + webview_request.Uri(&mut uri)?; + let uri = take_pwstr(uri); + + if let Some(custom_protocol) = custom_protocols + .iter() + .find(|(name, _)| uri.starts_with(&format!("https://{}.", name))) + { + // Undo the protocol workaround when giving path to resolver + let path = uri.replace( + &format!("https://{}.", custom_protocol.0), + &format!("{}://", custom_protocol.0), + ); + let final_request = request + .uri(&path) + .method(request_method.as_str()) + .body(body_sent) + .unwrap(); + + return match (custom_protocol.1)(&final_request) { + Ok(sent_response) => { + let content = sent_response.body(); + let status_code = sent_response.status().as_u16() as i32; + + let mut headers_map = String::new(); + + // set mime type if provided + if let Some(mime) = sent_response.mimetype() { + headers_map.push_str(&format!("Content-Type: {}\n", mime)) + } + + // build headers + for (name, value) in sent_response.headers().iter() { + let header_key = name.to_string(); + if let Ok(value) = value.to_str() { + headers_map.push_str(&format!("{}: {}\n", header_key, value)) + } + } + + let mut body_sent = None; + if !content.is_empty() { + let stream = CreateStreamOnHGlobal(0, true)?; + stream.SetSize(content.len() as u64)?; + if stream.Write(content.as_ptr() as *const _, content.len() as u32)? + as usize + == content.len() + { + body_sent = Some(stream); + } + } + + // FIXME: Set http response version + + let body_sent = body_sent.map(|content| content.cast().unwrap()); + let response = + env.CreateWebResourceResponse(body_sent, status_code, "OK", headers_map)?; + + args.SetResponse(response)?; + Ok(()) + } + Err(_) => Err(E_FAIL.into()), + }; + } + } + + Ok(()) + })), + &mut token, + ) + .map_err(webview2_com::Error::WindowsError)?; + } + } + + // Enable clipboard + if attributes.clipboard { + unsafe { + webview + .PermissionRequested( + PermissionRequestedEventHandler::create(Box::new(|_, args| { + if let Some(args) = args { + let mut kind = COREWEBVIEW2_PERMISSION_KIND_UNKNOWN_PERMISSION; + args.PermissionKind(&mut kind)?; + if kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ { + args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)?; + } + } + Ok(()) + })), + &mut token, + ) + .map_err(webview2_com::Error::WindowsError)?; + } + } + + // Set user agent + if let Some(user_agent) = attributes.user_agent { + unsafe { + let settings: ICoreWebView2Settings2 = webview + .Settings()? + .cast() + .map_err(webview2_com::Error::WindowsError)?; + settings.SetUserAgent(String::from(user_agent.as_str()))?; + } + } + + // Navigation + if let Some(url) = attributes.url { + if url.cannot_be_a_base() { + let s = url.as_str(); + if let Some(pos) = s.find(',') { + let (_, path) = s.split_at(pos + 1); + unsafe { + webview + .NavigateToString(path.to_string()) + .map_err(webview2_com::Error::WindowsError)?; + } + } + } else { + let mut url_string = String::from(url.as_str()); + let name = url.scheme(); + if custom_protocol_names.contains(name) { + // WebView2 doesn't support non-standard protocols yet, so we have to use this workaround + // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 + url_string = url + .as_str() + .replace(&format!("{}://", name), &format!("https://{}.", name)) + } + unsafe { + webview + .Navigate(url_string) + .map_err(webview2_com::Error::WindowsError)?; + } + } + } else if let Some(html) = attributes.html { + unsafe { + webview + .NavigateToString(html) + .map_err(webview2_com::Error::WindowsError)?; + } + } + + unsafe { + controller + .SetIsVisible(true) + .map_err(webview2_com::Error::WindowsError)?; + controller + .MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC) + .map_err(webview2_com::Error::WindowsError)?; + } + + Ok(webview) + } + + fn add_script_to_execute_on_document_created( + webview: &ICoreWebView2, + js: String, + ) -> webview2_com::Result<()> { + let handler_webview = webview.clone(); + AddScriptToExecuteOnDocumentCreatedCompletedHandler::wait_for_async_operation( + Box::new(move |handler| unsafe { + handler_webview + .AddScriptToExecuteOnDocumentCreated(js, handler) + .map_err(webview2_com::Error::WindowsError) + }), + Box::new(|_, _| Ok(())), + ) + } + + fn execute_script(webview: &ICoreWebView2, js: String) -> windows::core::Result<()> { + unsafe { + webview.ExecuteScript( + js, + ExecuteScriptCompletedHandler::create(Box::new(|_, _| (Ok(())))), + ) + } + } + pub fn print(&self) { let _ = self.eval("window.print()"); } pub fn eval(&self, js: &str) -> Result<()> { - if let Some(w) = self.webview.get() { - w.execute_script(js, |_| (Ok(())))?; - } - Ok(()) + Self::execute_script(&self.webview, js.to_string()) + .map_err(|err| Error::WebView2Error(webview2_com::Error::WindowsError(err))) } - pub fn resize(&self, hwnd: *mut c_void) -> Result<()> { - let hwnd = hwnd as HWND; - + pub fn resize(&self, hwnd: HWND) -> Result<()> { // Safety: System calls are unsafe // XXX: Resizing on Windows is usually sluggish. Many other applications share same behavior. unsafe { - let mut rect = std::mem::zeroed(); + let mut rect = RECT::default(); GetClientRect(hwnd, &mut rect); - if let Some(c) = self.controller.get() { - c.put_bounds(rect)?; - } + self.controller.SetBounds(rect) } - Ok(()) + .map_err(|error| Error::WebView2Error(webview2_com::Error::WindowsError(error))) } pub fn focus(&self) { - if let Some(c) = self.controller.get() { - let _ = c.move_focus(webview2::MoveFocusReason::Programmatic); - } + let _ = unsafe { + self + .controller + .MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC) + }; } } pub fn platform_webview_version() -> Result { - let webview_builder = webview2::EnvironmentBuilder::new(); - let version = webview_builder - .get_available_browser_version_string() - .expect("Unable to get webview2 version"); - Ok(version) + let mut versioninfo = PWSTR::default(); + unsafe { GetAvailableCoreWebView2BrowserVersionString(PWSTR::default(), &mut versioninfo) } + .map_err(webview2_com::Error::WindowsError)?; + Ok(take_pwstr(versioninfo)) +} + +fn is_windows_7() -> bool { + sys_info::os_release() + .map(|release| release.starts_with("6.1")) + .unwrap_or_default() } diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs index efcced1..656fe49 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/webview/wkwebview/mod.rs @@ -2,6 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#[cfg(target_os = "macos")] +mod file_drop; +mod web_context; + +pub use web_context::WebContextImpl; + #[cfg(target_os = "macos")] use cocoa::{ appkit::{NSView, NSViewHeightSizable, NSViewWidthSizable}, @@ -11,6 +17,7 @@ use cocoa::{ base::id, foundation::{NSDictionary, NSFastEnumeration}, }; + use std::{ ffi::{c_void, CStr}, os::raw::c_char, @@ -35,8 +42,11 @@ use file_drop::{add_file_drop_methods, set_file_drop_handler}; use crate::application::platform::ios::WindowExtIOS; use crate::{ - application::window::Window, - webview::{FileDropEvent, RpcRequest, RpcResponse, WebContext, WebViewAttributes}, + application::{ + dpi::{LogicalSize, PhysicalSize}, + window::Window, + }, + webview::{FileDropEvent, WebContext, WebViewAttributes}, Result, }; @@ -44,20 +54,14 @@ use crate::http::{ Request as HttpRequest, RequestBuilder as HttpRequestBuilder, Response as HttpResponse, }; -#[cfg(target_os = "macos")] -mod file_drop; - pub struct InnerWebView { - webview: Id, + webview: id, #[cfg(target_os = "macos")] ns_window: id, manager: id, // Note that if following functions signatures are changed in the future, // all fucntions pointer declarations in objc callbacks below all need to get updated. - rpc_handler_ptr: *mut ( - Box Option>, - Rc, - ), + ipc_handler_ptr: *mut (Box, Rc), #[cfg(target_os = "macos")] file_drop_ptr: *mut (Box bool>, Rc), protocol_ptrs: Vec<*mut Box Result>>, @@ -67,33 +71,23 @@ impl InnerWebView { pub fn new( window: Rc, attributes: WebViewAttributes, - _web_context: Option<&mut WebContext>, + mut web_context: Option<&mut WebContext>, ) -> Result { - // Function for rpc handler + // Function for ipc handler extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) { // Safety: objc runtime calls are unsafe unsafe { let function = this.get_ivar::<*mut c_void>("function"); - let function = &mut *(*function - as *mut ( - Box Fn(&'r Window, RpcRequest) -> Option>, - Rc, - )); - 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"); + if !function.is_null() { + let function = + &mut *(*function as *mut (Box Fn(&'r Window, String)>, Rc)); + 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"); - match super::rpc_proxy(&function.1, js.to_string(), &function.0) { - Ok(result) => { - let script = result.unwrap_or_default(); - let wv: id = msg_send![msg, webView]; - let js = NSString::new(&script); - let _: id = - msg_send![wv, evaluateJavaScript:js completionHandler:null::<*const c_void>()]; - } - Err(e) => { - eprintln!("{}", e); - } + (function.0)(&function.1, js.to_string()); + } else { + log::warn!("WebView instance is dropped! This handler shouldn't be called."); } } } @@ -102,93 +96,109 @@ impl InnerWebView { extern "C" fn start_task(this: &Object, _: Sel, _webview: id, task: id) { unsafe { let function = this.get_ivar::<*mut c_void>("function"); - let function = - &mut *(*function as *mut Box Fn(&'s HttpRequest) -> Result>); + if !function.is_null() { + let function = + &mut *(*function as *mut Box Fn(&'s HttpRequest) -> Result>); - // Get url request - let request: id = msg_send![task, request]; - let url: id = msg_send![request, URL]; - let nsstring = { - let s: id = msg_send![url, absoluteString]; - NSString(Id::from_ptr(s)) - }; + // Get url request + let request: id = msg_send![task, request]; + let url: id = msg_send![request, URL]; + let nsstring = { + let s: id = msg_send![url, absoluteString]; + NSString(Id::from_ptr(s)) + }; - // Get request method (GET, POST, PUT etc...) - let method = { - let s: id = msg_send![request, HTTPMethod]; - NSString(Id::from_ptr(s)) - }; + // Get request method (GET, POST, PUT etc...) + let method = { + let s: id = msg_send![request, HTTPMethod]; + NSString(Id::from_ptr(s)) + }; - // Prepare our HttpRequest - let mut http_request = HttpRequestBuilder::new() - .uri(nsstring.to_str()) - .method(method.to_str()); + // Prepare our HttpRequest + let mut http_request = HttpRequestBuilder::new() + .uri(nsstring.to_str()) + .method(method.to_str()); - // Get body - // FIXME: Add support of `HTTPBodyStream` - // https://developer.apple.com/documentation/foundation/nsurlrequest/1407341-httpbodystream?language=objc - let mut sent_form_body = Vec::new(); - let nsdata: id = msg_send![request, HTTPBody]; - // if we have a body - if !nsdata.is_null() { - let length = msg_send![nsdata, length]; - let data_bytes: id = msg_send![nsdata, bytes]; - sent_form_body = slice::from_raw_parts(data_bytes as *const u8, length).to_vec(); - } + // Get body + let mut sent_form_body = Vec::new(); + let body: id = msg_send![request, HTTPBody]; + let body_stream: id = msg_send![request, HTTPBodyStream]; + if !body.is_null() { + let length = msg_send![body, length]; + let data_bytes: id = msg_send![body, bytes]; + sent_form_body = slice::from_raw_parts(data_bytes as *const u8, length).to_vec(); + } else if !body_stream.is_null() { + let _: () = msg_send![body_stream, open]; - // Extract all headers fields - let all_headers: id = msg_send![request, allHTTPHeaderFields]; - - // get all our headers values and inject them in our request - for current_header_ptr in all_headers.iter() { - let header_field = NSString(Id::from_ptr(current_header_ptr)); - let header_value = NSString(Id::from_ptr(all_headers.valueForKey_(current_header_ptr))); - // inject the header into the request - http_request = http_request.header(header_field.to_str(), header_value.to_str()); - } - - // send response - let final_request = http_request.body(sent_form_body).unwrap(); - if let Ok(sent_response) = function(&final_request) { - let content = sent_response.body(); - // default: application/octet-stream, but should be provided by the client - let wanted_mime = sent_response.mimetype(); - // default to 200 - let wanted_status_code = sent_response.status().as_u16() as i32; - // default to HTTP/1.1 - let wanted_version = format!("{:#?}", sent_response.version()); - - let dictionary: id = msg_send![class!(NSMutableDictionary), alloc]; - let headers: id = msg_send![dictionary, initWithCapacity:1]; - if let Some(mime) = wanted_mime { - let () = msg_send![headers, setObject:NSString::new(mime) forKey: NSString::new("content-type")]; - } - let () = msg_send![headers, setObject:NSString::new(&content.len().to_string()) forKey: NSString::new("content-length")]; - - // add headers - for (name, value) in sent_response.headers().iter() { - let header_key = name.to_string(); - if let Ok(value) = value.to_str() { - let () = msg_send![headers, setObject:NSString::new(value) forKey: NSString::new(&header_key)]; + while msg_send![body_stream, hasBytesAvailable] { + sent_form_body.reserve(128); + let p = sent_form_body.as_mut_ptr().add(sent_form_body.len()); + let read_length = sent_form_body.capacity() - sent_form_body.len(); + let count: usize = msg_send![body_stream, read: p maxLength: read_length]; + sent_form_body.set_len(sent_form_body.len() + count); } + + let _: () = msg_send![body_stream, close]; } - let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc]; - let response: id = msg_send![urlresponse, initWithURL:url statusCode: wanted_status_code HTTPVersion:NSString::new(&wanted_version) headerFields:headers]; - let () = msg_send![task, didReceiveResponse: response]; + // Extract all headers fields + let all_headers: id = msg_send![request, allHTTPHeaderFields]; - // Send data - let bytes = content.as_ptr() as *mut c_void; - let data: id = msg_send![class!(NSData), alloc]; - let data: id = msg_send![data, initWithBytes:bytes length:content.len()]; - let () = msg_send![task, didReceiveData: data]; + // get all our headers values and inject them in our request + for current_header_ptr in all_headers.iter() { + let header_field = NSString(Id::from_ptr(current_header_ptr)); + let header_value = NSString(Id::from_ptr(all_headers.valueForKey_(current_header_ptr))); + // inject the header into the request + http_request = http_request.header(header_field.to_str(), header_value.to_str()); + } + + // send response + let final_request = http_request.body(sent_form_body).unwrap(); + if let Ok(sent_response) = function(&final_request) { + let content = sent_response.body(); + // default: application/octet-stream, but should be provided by the client + let wanted_mime = sent_response.mimetype(); + // default to 200 + let wanted_status_code = sent_response.status().as_u16() as i32; + // default to HTTP/1.1 + let wanted_version = format!("{:#?}", sent_response.version()); + + let dictionary: id = msg_send![class!(NSMutableDictionary), alloc]; + let headers: id = msg_send![dictionary, initWithCapacity:1]; + if let Some(mime) = wanted_mime { + let () = msg_send![headers, setObject:NSString::new(mime) forKey: NSString::new("content-type")]; + } + let () = msg_send![headers, setObject:NSString::new(&content.len().to_string()) forKey: NSString::new("content-length")]; + + // add headers + for (name, value) in sent_response.headers().iter() { + let header_key = name.to_string(); + if let Ok(value) = value.to_str() { + let () = msg_send![headers, setObject:NSString::new(value) forKey: NSString::new(&header_key)]; + } + } + + let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc]; + let response: id = msg_send![urlresponse, initWithURL:url statusCode: wanted_status_code HTTPVersion:NSString::new(&wanted_version) headerFields:headers]; + let () = msg_send![task, didReceiveResponse: response]; + + // Send data + let bytes = content.as_ptr() as *mut c_void; + let data: id = msg_send![class!(NSData), alloc]; + let data: id = msg_send![data, initWithBytes:bytes length:content.len()]; + let () = msg_send![task, didReceiveData: data]; + } else { + let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc]; + let response: id = msg_send![urlresponse, initWithURL:url statusCode:404 HTTPVersion:NSString::new("HTTP/1.1") headerFields:null::()]; + let () = msg_send![task, didReceiveResponse: response]; + } + // Finish + let () = msg_send![task, didFinish]; } else { - let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc]; - let response: id = msg_send![urlresponse, initWithURL:url statusCode:404 HTTPVersion:NSString::new("HTTP/1.1") headerFields:null::()]; - let () = msg_send![task, didReceiveResponse: response]; + log::warn!( + "Either WebView or WebContext instance is dropped! This handler shouldn't be called." + ); } - // Finish - let () = msg_send![task, didFinish]; } } extern "C" fn stop_task(_: &Object, _: Sel, _webview: id, _task: id) {} @@ -218,7 +228,11 @@ impl InnerWebView { }; let handler: id = msg_send![cls, new]; let function = Box::into_raw(Box::new(function)); - protocol_ptrs.push(function); + if let Some(context) = &mut web_context { + context.os.registered_protocols(function); + } else { + protocol_ptrs.push(function); + } (*handler).set_ivar("function", function as *mut _ as *mut c_void); let () = msg_send![config, setURLSchemeHandler:handler forURLScheme:NSString::new(&name)]; @@ -238,7 +252,6 @@ impl InnerWebView { let webview: id = msg_send![cls, alloc]; let preference: id = msg_send![config, preferences]; let yes: id = msg_send![class!(NSNumber), numberWithBool:1]; - let no: id = msg_send![class!(NSNumber), numberWithBool:0]; debug_assert_eq!( { @@ -250,12 +263,19 @@ impl InnerWebView { () ); + #[cfg(feature = "transparent")] if attributes.transparent { + let no: id = msg_send![class!(NSNumber), numberWithBool:0]; // Equivalent Obj-C: // [config setValue:@NO forKey:@"drawsBackground"]; let _: id = msg_send![config, setValue:no forKey:NSString::new("drawsBackground")]; } + #[cfg(feature = "fullscreen")] + // Equivalent Obj-C: + // [preference setValue:@YES forKey:@"fullScreenEnabled"]; + let _: id = msg_send![preference, setValue:yes forKey:NSString::new("fullScreenEnabled")]; + // Initialize webview with zero point let zero = CGRect::new(&CGPoint::new(0., 0.), &CGSize::new(0., 0.)); let _: () = msg_send![webview, initWithFrame:zero configuration:config]; @@ -267,7 +287,7 @@ impl InnerWebView { } // Message handler - let rpc_handler_ptr = if let Some(rpc_handler) = attributes.rpc_handler { + let ipc_handler_ptr = if let Some(ipc_handler) = attributes.ipc_handler { let cls = ClassDecl::new("WebViewDelegate", class!(NSObject)); let cls = match cls { Some(mut cls) => { @@ -281,12 +301,12 @@ impl InnerWebView { None => class!(WebViewDelegate), }; let handler: id = msg_send![cls, new]; - let rpc_handler_ptr = Box::into_raw(Box::new((rpc_handler, window.clone()))); + let ipc_handler_ptr = Box::into_raw(Box::new((ipc_handler, window.clone()))); - (*handler).set_ivar("function", rpc_handler_ptr as *mut _ as *mut c_void); - let external = NSString::new("external"); - let _: () = msg_send![manager, addScriptMessageHandler:handler name:external]; - rpc_handler_ptr + (*handler).set_ivar("function", ipc_handler_ptr as *mut _ as *mut c_void); + let ipc = NSString::new("ipc"); + let _: () = msg_send![manager, addScriptMessageHandler:handler name:ipc]; + ipc_handler_ptr } else { null_mut() }; @@ -307,11 +327,11 @@ impl InnerWebView { let ns_window = window.ns_window() as id; let w = Self { - webview: Id::from_ptr(webview), + webview, #[cfg(target_os = "macos")] ns_window, manager, - rpc_handler_ptr, + ipc_handler_ptr, #[cfg(target_os = "macos")] file_drop_ptr, protocol_ptrs, @@ -319,16 +339,19 @@ impl InnerWebView { // Initialize scripts w.init( - r#"window.external = { - invoke: function(s) { - window.webkit.messageHandlers.external.postMessage(s); - }, - };"#, +r#"Object.defineProperty(window, 'ipc', { + value: Object.freeze({postMessage: function(s) {window.webkit.messageHandlers.ipc.postMessage(s);}}) +});"#, ); for js in attributes.initialization_scripts { w.init(&js); } + // Set user agent + if let Some(user_agent) = attributes.user_agent { + w.set_user_agent(user_agent.as_str()) + } + // Navigation if let Some(url) = attributes.url { if url.cannot_be_a_base() { @@ -384,7 +407,10 @@ impl InnerWebView { unsafe { let userscript: id = msg_send![class!(WKUserScript), alloc]; let script: id = - msg_send![userscript, initWithSource:NSString::new(js) injectionTime:0 forMainFrameOnly:1]; + // FIXME: We allow subframe injection because webview2 does and cannot be disabled (currently). + // once webview2 allows disabling all-frame script injection, forMainFrameOnly should be enabled + // if it does not break anything. (originally added for isolation pattern). + msg_send![userscript, initWithSource:NSString::new(js) injectionTime:0 forMainFrameOnly:0]; let _: () = msg_send![self.manager, addUserScript: script]; } } @@ -406,6 +432,12 @@ impl InnerWebView { } } + fn set_user_agent(&self, user_agent: &str) { + unsafe { + let () = msg_send![self.webview, setCustomUserAgent: NSString::new(user_agent)]; + } + } + pub fn print(&self) { // Safety: objc runtime calls are unsafe #[cfg(target_os = "macos")] @@ -423,6 +455,14 @@ impl InnerWebView { } pub fn focus(&self) {} + + #[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 as f64, view_frame.size.height as f64).into(); + logical.to_physical(scale_factor) + } } pub fn platform_webview_version() -> Result { @@ -441,8 +481,8 @@ impl Drop for InnerWebView { fn drop(&mut self) { // We need to drop handler closures here unsafe { - if !self.rpc_handler_ptr.is_null() { - let _ = Box::from_raw(self.rpc_handler_ptr); + if !self.ipc_handler_ptr.is_null() { + let _ = Box::from_raw(self.ipc_handler_ptr); } #[cfg(target_os = "macos")] @@ -455,6 +495,11 @@ impl Drop for InnerWebView { let _ = Box::from_raw(*ptr); } } + + let _: Id<_> = Id::from_ptr(self.webview); + #[cfg(target_os = "macos")] + let _: Id<_> = Id::from_ptr(self.ns_window); + let _: Id<_> = Id::from_ptr(self.manager); } } } diff --git a/src/webview/wkwebview/web_context.rs b/src/webview/wkwebview/web_context.rs new file mode 100644 index 0000000..f74a4b0 --- /dev/null +++ b/src/webview/wkwebview/web_context.rs @@ -0,0 +1,38 @@ +use crate::Result; + +use crate::{ + http::{Request, Response}, + webview::web_context::WebContextData, +}; + +#[derive(Debug)] +pub struct WebContextImpl { + protocols: Vec<*mut Box Result>>, +} + +impl WebContextImpl { + pub fn new(_data: &WebContextData) -> Self { + Self { + protocols: Vec::new(), + } + } + + pub fn set_allows_automation(&mut self, _flag: bool) {} + + pub fn registered_protocols(&mut self, handler: *mut Box Result>) { + self.protocols.push(handler); + } +} + +impl Drop for WebContextImpl { + fn drop(&mut self) { + // We need to drop handler closures here + unsafe { + for ptr in self.protocols.iter() { + if !ptr.is_null() { + let _ = Box::from_raw(*ptr); + } + } + } + } +}