Merge next back to dev branch (#477)

* refactor: remove unnecessary private mod shared (#400)

* refactor: remove unnecessary private mod shared

* chore: make fmt happy

* refactor: use eventloop proxy in `custom_titlebar` example (#401)

* Switch to tauri-webview2 (#411)

* Switch to tauri-webview2

This is a temporary fix of null pointer crash on `get_content` of web resource request. We will switch it back once upstream is updated.

* Add change file

* fix: remove clipboard property for consistency across platforms (#412)

* Remove clipboard property for consistancy across platforms

* Remove clipboard property for consistency

* feat: add inner_size for webview struct (#394)

* Add inner_size method

* Add inner_size for webview

* Fix feature flag placement

* Add feature flag on inner_size

* Cargo fmt

* feat(linux): allow resizing undecorated window using touch, closes #399 (#402)

* feat(linux): allow resizing undecorated window using touch
closes #399

* fix windows

* update custom_titlebar example

* revert windows changes

* Implement WebContextImpl on mac (#372)

* Implement WebContextImpl on mac

* Update type visibility

* Update cfg declaration

* Move WebContextImpl to wkwebview

* Fix protocol signature

* Add missing feature flag

* Cargo fmt

* Add change file

* Replace winapi with windows crate and use webview2-com instead of webview2 (#414)

* Add Windows and webview2-com  crates

* Replace winapi and webview2-sys with webview2-com

* Get all the examples working with webview2-com

* Point to the published version of webview2-com

* Add a summary in .changes

* Remove extra projects not in config.json

* Run cargo fmt --all

* Fix clippy warnings

* Update to 32-bit compatible webview2-com

* Sync with refactor in next branch of tao

* Use path prefix to prevent variable shadowing

* Fix Linux nightly/stable clippy warnings

* Update to latest review feedback in TAO

* Replace tao dependency with git+branch

* Switch to next branch of upstream tao repo

* doc: add warning doc on webcontext and ptr detections (#419)

* Add warning doc on webcontext and ptr detections

* cargo fmt

* Move Unix's webcontext implementation to webkitgtk (#421)

* chore: add `on_issue_closed.yml` (#424)

* Update webkit2gtk to 0.15 (#429)

* Update webkit2gtk

* Update webkit2gtk to 0.15

* Add changes file

* fix: add feature flags to toggle private APIs (#431)

* Add transparent feature flag

* Add flag for video fullscreen on macOS

* Add change file

* Make clippy happy

* Add clipboard field in WebViewAttributes (#432)

* fix(webview/linux): Use a workaround to fix CORS ... (#435)

... when making XHR requests inside a custom URI scheme context.

Reference: 9b6eae32a6/Source/WebKit/UIProcess/API/glib/WebKitProtocolHandler.cpp (L82-L91)

* Delete on_issue_closed.yml (#436)

* Enable secure context back (#438)

* Disable all option on custom url scheme for now

* Enable secure context back

* fix link to tao repo (#439)

* fix(deps): update rust crate http to 0.2.5 (#416)

Co-authored-by: Renovate Bot <bot@renovateapp.com>

* fix link to tao

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <wusyong9104@gmail.com>

* chore: remove unused files (#440)

* Update webview2-com and windows crates (#446)

* Replace webivew2-com-sys with prebuilt windows

* Bump to latest versions

* Set file drop handler after window creation

* Point tao back to the upstream next branch

* Rerun cargo fmt and fix Windows clippy warnings

* Add changelog file

* Use aliases for webview2_com instead of windows

* Use time instead of chrono (#447)

* Use time instead of chrono

* Make clippy happy

* fix(macos): unsupported key feedback sound (#448)

* fix(macos): unsupported key feedback sound, closes #

* Fix scope of YES

Co-authored-by: Yu-Wei Wu <wusyong9104@gmail.com>

* Revert "fix(macos): unsupported key feedback sound (#448)"

This reverts commit b8fdfd6aa1.

* Use tao commit with windows 0.25

* Switch back to tao's next branch

* feat: enable cookie persistence on Linux (#453)

* feat: enable cookie persistence on Linux

* fmt

* Bump webkit2gtk version (#457)

* Update windows crate to 0.29.0 and webview2-com to 0.9.0 (#455)

* Update to windows 0.29.0

* Add change description

* Update tao dependency (#461)

* fix: ignore transparency on Windows 7 (#460)

* fix: ignore transparency on Windows 7

* Update Cargo.toml

Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <wusyong9104@gmail.com>

* Remove clippy check (#462)

* chore: update PR template

* Fix: #276 - Add ability to set custom User Agent (#464)

* feat: Custom user agent attribute

Included example

* Fix setting type on Windows

Co-authored-by: Adit Sachde <23707194+aditsachde@users.noreply.github.com>
Co-authored-by: Wu Yu Wei <wusyong9104@gmail.com>

* Add support of `HTTPBodyStream` (#465)

* Update to 2021 edition (#466)

* Update windows crate to 0.30.0

* change tao dep

* remove "tray" from default features and allow using "ayatana" instead (#469)

The "tray" feature enables dependency on libappindicator3.

On Debian 11 (and some other systems), libappindicator3 was
removed and replaced with libayatana-appindicator3:
  https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html#noteworthy-obsolete-packages

This new library can be used by enabling the "ayatana" feature.

Depending on the system, only one of those library will usually
be available, and thus, only one of the "tray" and "ayatana"
feature would compile fine.

Having "tray" in default features did prevent building on systems
with no libappindicator3 available.

* Update tray examples (#470)

* chore: update pull request commit exmple

* feat: enable objc's exception feature [TRI-039]

* refactor: IPC handler [TRI-019]

* Feat/isolation (#5)

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <wusyong9104@gmail.com>
Co-authored-by: Yu-Wei Wu <wusyong9104@gmail.com>
Co-authored-by: Adit Sachde <23707194+aditsachde@users.noreply.github.com>
Co-authored-by: Wu Yu Wei <wusyong9104@gmail.com>
Co-authored-by: Amr Bashir <48618675+amrbashir@users.noreply.github.com>
Co-authored-by: Bogdan Bivolaru <104334+bogdanbiv@users.noreply.github.com>
Co-authored-by: Bill Avery <wravery@users.noreply.github.com>
Co-authored-by: Lucas Fernandes Nogueira <lucasfernandesnog@gmail.com>
Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.studio>
Co-authored-by: amrbashir <amr.bashir2015@gmail.com>
Co-authored-by: Chip Reed <chip@chip.sh>
Co-authored-by: Jhonatan A <1548170+baguio@users.noreply.github.com>

* chore: fix Linux clippy warnings

* chore(audit): add report

* fix: windows build

* Update gtk to 0.15 (#472)

* chore: new `multi_window` example, closes #459 (#476)

* chore: new `multi_window` example, closes #459

* revert license change

Co-authored-by: Jason <jason@pews.dev>
Co-authored-by: Amr Bashir <48618675+amrbashir@users.noreply.github.com>
Co-authored-by: Bill Avery <wravery@users.noreply.github.com>
Co-authored-by: liushuyu <liushuyu011@gmail.com>
Co-authored-by: Bogdan Bivolaru <104334+bogdanbiv@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Lucas Fernandes Nogueira <lucasfernandesnog@gmail.com>
Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.studio>
Co-authored-by: amrbashir <amr.bashir2015@gmail.com>
Co-authored-by: Jhonatan A <1548170+baguio@users.noreply.github.com>
Co-authored-by: Adit Sachde <23707194+aditsachde@users.noreply.github.com>
Co-authored-by: Michael Alyn Miller <malyn@strangeGizmo.com>
Co-authored-by: Bill Avery <wravery@gmail.com>
Co-authored-by: Aurélien Jacobs <aurel@gnuage.org>
Co-authored-by: Chip Reed <chip@chip.sh>
This commit is contained in:
Ngo Iok Ui (Wu Yu Wei)
2022-02-05 21:04:07 +08:00
committed by GitHub
parent 3a6eefae66
commit 219d20ce66
52 changed files with 1731 additions and 1567 deletions

5
.changes/15.md Normal file
View File

@@ -0,0 +1,5 @@
---
"wry": minor
---
Update gtk to 0.15

6
.changes/clipboard.md Normal file
View File

@@ -0,0 +1,6 @@
---
"wry": patch
---
Add clipboard field in WebViewAttributes.

View File

@@ -0,0 +1,5 @@
---
"wry": patch
---
Ignore transparency on Windows 7 to prevent application crash.

View File

@@ -0,0 +1,5 @@
---
"wry": patch
---
Remove clipboard property for consistency across platforms.

View File

@@ -0,0 +1,5 @@
---
"wry": patch
---
Enable cookie persistence on Linux if the `data_directory` is provided.

View File

@@ -0,0 +1,6 @@
---
"wry": patch
---
Enable objc's exception features so they can be treated as panic message.

View File

@@ -0,0 +1,5 @@
---
"wry": patch
---
Add inner size method for webview. This can reflect correct size of webview on macOS.

6
.changes/mac-priv.md Normal file
View File

@@ -0,0 +1,6 @@
---
"wry": patch
---
Add "transparent" and "fullscreen" featrue flags on macOS to toggle private API.

View File

@@ -0,0 +1,6 @@
---
"wry": patch
---
Implement WebContextImpl on mac to extend several callback lifetimes.

View File

@@ -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.

View File

@@ -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.

6
.changes/webkit2gtk.md Normal file
View File

@@ -0,0 +1,6 @@
---
"wry": patch
---
Update webkit2gtk to 0.15

View File

@@ -0,0 +1,4 @@
---
"wry": minor
---
Add `with_user_agent(&str)` to `WebViewBuilder`.

5
.changes/webview2-com.md Normal file
View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -0,0 +1,5 @@
---
"wry": patch
---
Update the `windows` crate to 0.29.0 and `webview2-com` to 0.9.0.

View File

@@ -0,0 +1,5 @@
---
"wry": patch
---
Update the `windows` crate to 0.30.0 and `webview2-com` to 0.10.0.

View File

@@ -1,36 +1,30 @@
<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/tauri-apps/tauri/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
Update "[ ]" to "[x]" to check a box
Please make sure to read the Pull Request Guidelines: https://github.com/tauri-apps/tauri/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->
<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->
**What kind of change does this PR introduce?** (check at least one)
### What kind of change does this PR introduce?
<!-- Check at least one. If you are introducing a new binding, you must reference an issue where this binding has been proposed, discussed and approved by the maintainers. -->
- [ ] 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)
<!--
If yes, please describe the impact and migration path for existing applications in an attached issue. Filing a PR with breaking changes that has not been discussed and approved by the maintainers in an issue will be immediately closed.
-->
### Does this PR introduce a breaking change?
<!-- If yes, please describe the impact and migration path for existing applications in an attached issue. -->
- [ ] 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

View File

@@ -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 }}

View File

@@ -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:

View File

@@ -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"

View File

@@ -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

Binary file not shown.

View File

@@ -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" ] }

View File

@@ -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()

View File

@@ -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<()> {
}
});
}

View File

@@ -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#"
<!DOCTYPE html>
@@ -46,7 +45,7 @@ fn main() -> wry::Result<()> {
<h1>Welcome to WRY!</h1>
<script>
document.addEventListener('DOMContentLoaded', () => {
rpc.call('dom-loaded')
ipc.postMessage('dom-loaded')
})
</script>
</body>

View File

@@ -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,
<script>
document.addEventListener('DOMContentLoaded', () => {
rpc.call('dom-loaded')
ipc.postMessage('dom-loaded')
})
</script>
"#;
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| {

View File

@@ -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 <code>${message.data.count}</code> prime numbers in <code>${message.data.time}ms</code>`;

View File

@@ -11,7 +11,6 @@ Run the `cargo run --example <file_name>` 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.

View File

@@ -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::<UserEvents>::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
}
}
_ => (),
}
});
}

View File

@@ -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());
}
}
});
}

View File

@@ -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::<String>();
let handler = move |_window: &Window, req: RpcRequest| {
if &req.method == "openWindow" {
if let Some(params) = req.params {
if let Value::String(url) = &params[0] {
let _ = window_tx.send(url.to_string());
}
fn create_new_window(
title: String,
event_loop: &EventLoopWindowTarget<UserEvents>,
proxy: EventLoopProxy<UserEvents>,
) -> (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#"
<button onclick="window.ipc.postMessage('new-window')">Open a new window</button>
<button onclick="window.ipc.postMessage('close')">Close current window</button>
<input oninput="window.ipc.postMessage(`change-title:${this.value}`)" />
"#,
)
.unwrap()
.with_ipc_handler(handler)
.build()
.unwrap();
(window_id, webview)
}
let event_loop = EventLoop::<UserEvents>::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
}
}
_ => (),
}
});
}

View File

@@ -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,
<script>
let fullscreen = false;
async function toggleFullScreen() {
await rpc.call('fullscreen', !fullscreen);
fullscreen = !fullscreen;
}
async function getAsyncRpcResult() {
const reply = await rpc.call('send-parameters', {'message': 'WRY'});
const result = document.getElementById('rpc-result');
result.innerText = reply;
}
</script>
<div><button onclick="toggleFullScreen();">Toggle fullscreen</button></div>
<div><button onclick="getAsyncRpcResult();">Send parameters</button></div>
<div id="rpc-result"></div>
"#;
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::<Vec<bool>>(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::<Vec<MessageParameters>>(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();
}
}
});
}

View File

@@ -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.");
}

View File

@@ -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.");
}

45
examples/user_agent.rs Normal file
View File

@@ -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,
_ => (),
}
});
}

Binary file not shown.

View File

@@ -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<K, V>(self, key: K, value: V) -> Builder
where
HeaderName: TryFrom<K>,

View File

@@ -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<T> = std::result::Result<T, Error>;
@@ -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}")]

View File

@@ -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;

View File

@@ -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<String>,
/// 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<Url>,
@@ -89,17 +93,11 @@ pub struct WebViewAttributes {
///
/// [bug]: https://bugs.webkit.org/show_bug.cgi?id=229034
pub custom_protocols: Vec<(String, Box<dyn Fn(&HttpRequest) -> Result<HttpResponse>>)>,
/// 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<Box<dyn Fn(&Window, RpcRequest) -> Option<RpcResponse>>>,
pub ipc_handler: Option<Box<dyn Fn(&Window, String)>>,
/// 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<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
#[cfg(not(feature = "file-drop"))]
file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> 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<F>(mut self, handler: F) -> Self
pub fn with_ipc_handler<F>(mut self, handler: F) -> Self
where
F: Fn(&Window, RpcRequest) -> Option<RpcResponse> + '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<WebView> {
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<WebView> {
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<RpcResponse>,
) -> Result<Option<String>> {
let req = serde_json::from_str::<RpcRequest>(&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<u32> {
#[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<Value>,
pub method: String,
pub params: Option<Value>,
}
/// RPC response message which being sent back to the Javascript side.
#[derive(Debug, Serialize, Deserialize)]
pub struct RpcResponse {
jsonrpc: String,
pub(crate) id: Option<Value>,
pub(crate) result: Option<Value>,
pub(crate) error: Option<Value>,
}
impl RpcResponse {
/// Create a new result response.
pub fn new_result(id: Option<Value>, result: Option<Value>) -> Self {
Self {
jsonrpc: RPC_VERSION.to_string(),
id,
result,
error: None,
}
}
/// Create a new error response.
pub fn new_error(id: Option<Value>, error: Option<Value>) -> 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<String> {
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<String> {
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<String> {
#[cfg(target_os = "windows")]
pub trait WebviewExtWindows {
/// Returns WebView2 Controller
fn controller(&self) -> Option<&::webview2::Controller>;
fn controller(&self) -> Option<ICoreWebView2Controller>;
}
#[cfg(target_os = "windows")]
impl WebviewExtWindows for WebView {
fn controller(&self) -> Option<&::webview2::Controller> {
self.webview.controller.get()
fn controller(&self) -> Option<ICoreWebView2Controller> {
Some(self.webview.controller.clone())
}
}

View File

@@ -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<PathBuf>,
}
@@ -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<WebviewUriLoader>,
registered_protocols: HashSet<String>,
automation: bool,
app_info: Option<ApplicationInfo>,
}
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<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + '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<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + 'static;
/// Add a [`WebView`] to the queue waiting to be opened.
///
/// See the `WebviewUriLoader` for more information.
fn queue_load_uri(&self, webview: Rc<WebView>, 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<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + '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<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + '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<WebView>, 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<F>(
context: &mut super::WebContext,
name: &str,
handler: F,
) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + '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<VecDeque<(Rc<WebView>, 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<WebView>, 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<WebView>, 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<Self>) {
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();
}
}
}
}
}

View File

@@ -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<WebView>,
@@ -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,
&[],

View File

@@ -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<WebviewUriLoader>,
registered_protocols: HashSet<String>,
automation: bool,
app_info: Option<ApplicationInfo>,
}
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<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + '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<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + 'static;
/// Add a [`WebView`] to the queue waiting to be opened.
///
/// See the `WebviewUriLoader` for more information.
fn queue_load_uri(&self, webview: Rc<WebView>, 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<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + '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<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + '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<WebView>, 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<F>(
context: &mut super::WebContext,
name: &str,
handler: F,
) -> crate::Result<()>
where
F: Fn(&HttpRequest) -> crate::Result<HttpResponse> + '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<VecDeque<(Rc<WebView>, 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<WebView>, 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<WebView>, 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<Self>) {
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();
}
}
}
}

View File

@@ -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<IDropTarget>,
}
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<dyn Fn(&Window, FileDropEvent) -> bool>,
refcount: AtomicUsize,
hwnd: HWND,
#[implement(Windows::Win32::System::Ole::IDropTarget)]
pub struct FileDropHandler {
window: Rc<Window>,
cursor_effect: DWORD,
listener: Rc<dyn Fn(&Window, FileDropEvent) -> 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<Window>,
listener: Rc<dyn Fn(&Window, FileDropEvent) -> 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<IDataObject>,
_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<IDataObject>,
_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<IDataObject>,
paths: &mut Vec<PathBuf>,
) -> Option<shellapi::HDROP> {
use winapi::{
shared::{
winerror::{DV_E_FORMATETC, SUCCEEDED},
wtypes::{CLIPFORMAT, DVASPECT_CONTENT},
},
um::{
objidl::{FORMATETC, TYMED_HGLOBAL},
shellapi::DragQueryFileW,
winuser::CF_HDROP,
},
};
) -> Option<HDROP> {
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,
};

View File

@@ -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<webview2_com::Error> for Error {
fn from(err: webview2_com::Error) -> Self {
Error::WebView2Error(err)
}
}
pub struct InnerWebView {
pub(crate) controller: Rc<OnceCell<Controller>>,
webview: Rc<OnceCell<WebView>>,
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<Self> {
let hwnd = window.hwnd() as HWND;
let controller: Rc<OnceCell<Controller>> = Rc::new(OnceCell::new());
let controller_clone = controller.clone();
let hwnd = HWND(window.hwnd() as _);
let file_drop_controller: Rc<OnceCell<FileDropController>> = 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<OnceCell<WebView>> = 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<ICoreWebView2Environment> {
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<ICoreWebView2Controller> {
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<Window>,
hwnd: HWND,
mut attributes: WebViewAttributes,
env: &ICoreWebView2Environment,
controller: &ICoreWebView2Controller,
) -> webview2_com::Result<ICoreWebView2> {
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<String> {
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()
}

View File

@@ -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<Object>,
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<dyn Fn(&Window, RpcRequest) -> Option<RpcResponse>>,
Rc<Window>,
),
ipc_handler_ptr: *mut (Box<dyn Fn(&Window, String)>, Rc<Window>),
#[cfg(target_os = "macos")]
file_drop_ptr: *mut (Box<dyn Fn(&Window, FileDropEvent) -> bool>, Rc<Window>),
protocol_ptrs: Vec<*mut Box<dyn Fn(&HttpRequest) -> Result<HttpResponse>>>,
@@ -67,33 +71,23 @@ impl InnerWebView {
pub fn new(
window: Rc<Window>,
attributes: WebViewAttributes,
_web_context: Option<&mut WebContext>,
mut web_context: Option<&mut WebContext>,
) -> Result<Self> {
// 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<dyn for<'r> Fn(&'r Window, RpcRequest) -> Option<RpcResponse>>,
Rc<Window>,
));
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<dyn for<'r> Fn(&'r Window, String)>, Rc<Window>));
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<dyn for<'s> Fn(&'s HttpRequest) -> Result<HttpResponse>>);
if !function.is_null() {
let function =
&mut *(*function as *mut Box<dyn for<'s> Fn(&'s HttpRequest) -> Result<HttpResponse>>);
// 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::<c_void>()];
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::<c_void>()];
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<u32> {
let view_frame = unsafe { NSView::frame(self.webview) };
let logical: LogicalSize<f64> =
(view_frame.size.width as f64, view_frame.size.height as f64).into();
logical.to_physical(scale_factor)
}
}
pub fn platform_webview_version() -> Result<String> {
@@ -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);
}
}
}

View File

@@ -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<dyn Fn(&Request) -> Result<Response>>>,
}
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<dyn Fn(&Request) -> Result<Response>>) {
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);
}
}
}
}
}