Refactor new method entry point to rwh (#1041)

* Refactor new method entry point to rwh

* Update doc and add change file

* Fix mac and ios compile error

* Add new_as_child

* Add rhw_04, 05 06 flags

* Update android port

* Remove winit CI

* Fix android bindings

* windows implementation

* Update Cargo.toml

* Fix macOS and iOS

* Fix Android

* fix is_child on WkWebView

* x11 impl

* expose `new_gtk`

* fix winit-gtk version

* remove undecorated resizing handling

* implement set_position and set_size for child gtk in x11

* fix macos

* more fixes

* one more time

* unreachable

* update actions

* fix windows

* some clippy

* Add documentation and fix clippy

* Fix windows clippy error

* Fix android biuld

* Fix documentation test

* Fix android again

* Reduce clippy noise

* use rc?

* refine some documentation, add set_visible

* fix set_visible

* impl set_visible on Windows

* fix doctests

* more set_visible fixes

* fix windows

* unsafe

* fix set size for child webviews

* fix initial visibility with new_gtk

* refine examples

* fix wpgu example

* fix examples on windows and macos

* use a better workaround

* make set_size work on x11

* Fix size in multiwebview example

* Add visible method on macOS and iOS

* remvoe `tao` from android backend and update documentation

* fix winit example

* Add new_as_content_view on macOS

* allow using raw-window-handle v0.5 [skip ci]

* change trait name [skip ci]

* fix linux impl [skip ci]

* fix android build [skip ci]

* fix windows build

* fix(macos): do not autoresize on child webview [skip ci]

* fix macos coordinates [skip ci]

* fix example [skip ci]

* fixed child on macos [skip ci]

* fix docs typos [skip ci]

* fix webview position when it's not a child [skip ci]

* replace new_as_content_view with new_as_subview

* with_as_subview instead of constructor [skip ci]

* fix position/size when as_subview is true

* lint & fmt

* Fix ios build

* Fix cargo test

* Fix mac build

* Update macOS contrusctors

* cargo fmt

* impl drop on Windows

* fix child transparency on Windows (still needs PRs in tao and winit)

* fix winit usage in the examples, use rwh_05 only for now

* fix tests

* add webview.focus

* fix dropping on Linux

* chore clean examples

* implement focus on Linux

* macos focus

---------

Co-authored-by: amrbashir <amr.bashir2015@gmail.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
Ngo Iok Ui (Wu Yu Wei)
2023-11-07 20:30:35 +09:00
committed by GitHub
parent 81cfa37e32
commit 783b14239d
71 changed files with 3445 additions and 3468 deletions

17
.changes/rwh.md Normal file
View File

@@ -0,0 +1,17 @@
---
"wry": minor
---
Refactor new method to take raw window handle instead. Following are APIs got affected:
- `application` module is removed, and `webivew` module is moved to root module.
- `WebviewBuilder::new`, `Webview::new` now take `RawWindowHandle` instead.
- Attributes `ipc_handler`, `file_drop_handler`, `document_change_handler` don't have window parameter anymore.
Users should use closure to capture the types they want to use.
- Position field in `FileDrop` event is now `Position` instead of `PhysicalPosition`. Users need to handle scale factor
depend on the situation they have.
- `Webview::inner_size` is removed.
This also means that we removed `tao` as a dependency completely which required some changes to the Android backend:
- We exposed the `android_setup` function that needs to be called once to setup necessary logic.
- Previously the `android_binding!` had internal call to `tao::android_binding` but now that `tao` has been removed,sa
the macro signature has changed and you now need to call `tao::android_binding` yourself, checkout the crate documentation for more information.

View File

@@ -1,20 +1,33 @@
name: Audit
# Copyright 2022-2022 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
name: audit
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
push:
branches:
- dev
paths:
- "Cargo.lock"
- "Cargo.toml"
- 'Cargo.lock'
- 'Cargo.toml'
pull_request:
paths:
- 'Cargo.lock'
- 'Cargo.toml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
audit-rust:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: rust audit
uses: actions-rs/audit-check@v1
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -7,7 +7,6 @@ jobs:
strategy:
fail-fast: false
matrix:
rust_version: [stable]
platform:
- { target: x86_64-pc-windows-msvc, os: windows-latest }
- { target: x86_64-unknown-linux-gnu, os: ubuntu-latest }
@@ -28,7 +27,7 @@ jobs:
if: contains(matrix.platform.target, 'gnu')
run: |
sudo apt-get update
sudo apt-get install -y webkit2gtk-4.1 libgtksourceview-3.0-dev libayatana-appindicator3-dev
sudo apt-get install -y libwebkit2gtk-4.1-dev
- name: install webview2 (windows only)
if: contains(matrix.platform.target, 'windows')
@@ -44,20 +43,10 @@ jobs:
- name: build tests and examples
shell: bash
if: (
!contains(matrix.platform.target, 'android') &&
!contains(matrix.platform.target, 'ios'))
if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios'))
run: cargo test --no-run --verbose --target ${{ matrix.platform.target }}
- name: run tests
if: (
!contains(matrix.platform.target, 'android') &&
!contains(matrix.platform.target, 'ios'))
if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios'))
run: cargo test --verbose --target ${{ matrix.platform.target }} --features linux-body
- name: build wry with winit
if: (
contains(matrix.platform.target, 'gnu') ||
contains(matrix.platform.target, 'windows') ||
contains(matrix.platform.target, 'apple'))
run: cargo build --no-default-features --features winit --target ${{ matrix.platform.target }}

48
.github/workflows/clippy-fmt.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
# Copyright 2022-2022 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
name: clippy & fmt
on:
push:
branches:
- dev
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
clippy:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: install system deps
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: cargo clippy --all-targets
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check

View File

@@ -6,9 +6,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/checkout@v4
- name: covector status
uses: jbolda/covector/packages/action@covector-v0
id: covector

View File

@@ -1,4 +1,4 @@
name: version or publish
name: covector version or publish
on:
push:
@@ -15,15 +15,16 @@ jobs:
successfulPublish: ${{ steps.covector.outputs.successfulPublish }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/checkout@v4
- name: cargo login
run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }}
- name: git config
run: |
git config --global user.name "${{ github.event.pusher.name }}"
git config --global user.email "${{ github.event.pusher.email }}"
- name: covector version or publish (publish when no change files present)
uses: jbolda/covector/packages/action@covector-v0
id: covector
@@ -34,14 +35,15 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
command: 'version-or-publish'
createRelease: true
- name: Create Pull Request With Versions Bumped
id: cpr
uses: tauri-apps/create-pull-request@v3
if: steps.covector.outputs.commandRan == 'version'
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: "Publish New Versions"
commit-message: "publish new versions"
labels: "version updates"
branch: "release"
title: Apply Version Updates From Current Changes
commit-message: 'apply version updates'
labels: 'version updates'
branch: 'release'
body: ${{ steps.covector.outputs.change }}

View File

@@ -1,31 +0,0 @@
name: fmt check
on:
pull_request:
paths:
- 'src/**'
- 'Cargo.toml'
jobs:
clippy_fmt_check:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt,clippy
- uses: Swatinem/rust-cache@v2
- name: fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all --examples -- -D warnings

View File

@@ -1,71 +1,67 @@
workspace = { }
workspace = {}
[package]
name = "wry"
version = "0.34.2"
authors = [ "Tauri Programme within The Commons Conservancy" ]
authors = ["Tauri Programme within The Commons Conservancy"]
edition = "2021"
license = "Apache-2.0 OR MIT"
description = "Cross-platform WebView rendering library"
readme = "README.md"
repository = "https://github.com/tauri-apps/wry"
documentation = "https://docs.rs/wry"
categories = [ "gui" ]
categories = ["gui"]
[package.metadata.docs.rs]
no-default-features = true
features = [ "tao", "file-drop", "protocol" ]
features = ["file-drop", "protocol"]
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"x86_64-apple-darwin"
"x86_64-apple-darwin",
]
rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
rustc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = [ "file-drop", "objc-exception", "protocol", "tao" ]
objc-exception = [ "objc/exception" ]
file-drop = [ ]
protocol = [ ]
devtools = [ ]
transparent = [ ]
fullscreen = [ ]
linux-body = [ "webkit2gtk/v2_40" ]
mac-proxy = [ ]
default = ["file-drop", "objc-exception", "protocol"]
objc-exception = ["objc/exception"]
file-drop = []
protocol = []
devtools = []
transparent = []
fullscreen = []
linux-body = ["webkit2gtk/v2_40"]
mac-proxy = []
[dependencies]
libc = "0.2"
log = "0.4"
once_cell = "1"
serde = { version = "1.0", features = [ "derive" ] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
url = "2.4"
tao = { version = "0.23", default-features = false, features = [ "serde" ], optional = true }
http = "0.2.9"
[dev-dependencies]
http-range = "0.1.5"
base64 = "0.21"
http = "0.2"
raw-window-handle = { version = "0.5", features = ["std"] }
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
javascriptcore-rs = { version = "=1.1", features = [ "v2_28" ] }
webkit2gtk = { version = "=2.0", features = [ "v2_38" ] }
javascriptcore-rs = { version = "=1.1", features = ["v2_28"] }
webkit2gtk = { version = "=2.0", features = ["v2_38"] }
webkit2gtk-sys = "=2.0"
gtk = "0.18"
soup3 = "0.5"
winit = { version = "0.29", package = "winit-gtk", features = [ "serde" ], optional = true }
x11-dl = "2.9"
gdkx11 = "0.18"
[target."cfg(target_os = \"windows\")".dependencies]
webview2-com = "0.27"
windows-implement = "0.51"
dunce = "1"
winit = { version = "0.28", features = [ "serde" ], optional = true }
[target."cfg(target_os = \"windows\")".dependencies.windows]
version = "0.51"
features = [
[target."cfg(target_os = \"windows\")".dependencies.windows]
version = "0.51"
features = [
"implement",
"Win32_Foundation",
"Win32_Graphics_Gdi",
@@ -80,7 +76,7 @@ winit = { version = "0.28", features = [ "serde" ], optional = true }
"Win32_Globalization",
"Win32_UI_HiDpi",
"Win32_UI_Input",
"Win32_UI_Input_KeyboardAndMouse"
"Win32_UI_Input_KeyboardAndMouse",
]
[target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies]
@@ -89,7 +85,6 @@ cocoa = "0.25"
core-graphics = "0.23"
objc = "0.2"
objc_id = "0.1"
winit = { version = "0.28", features = [ "serde" ], optional = true }
[target."cfg(target_os = \"android\")".dependencies]
crossbeam-channel = "0.5"
@@ -97,3 +92,15 @@ html5ever = "0.26"
kuchiki = { package = "kuchikiki", version = "0.8" }
sha2 = "0.10"
base64 = "0.21"
jni = "0.21"
ndk = "0.7"
ndk-sys = "0.4"
ndk-context = "0.1"
tao-macros = { version = "0.1.0" }
[dev-dependencies]
http-range = "0.1.5"
pollster = "0.3.0"
tao = { git = "https://github.com/tauri-apps/tao", default-features = false, features = ["rwh_05"] }
wgpu = "0.18.0"
winit = { version = "0.29", features = ["rwh_05"] }

View File

@@ -117,7 +117,34 @@ WebView2 provided by Microsoft Edge Chromium is used. So wry supports Windows 7,
Wry supports mobile with the help of [`cargo-mobile2`](https://github.com/tauri-apps/cargo-mobile2) CLI to create template project. If you are interested in playing or hacking it, please follow [MOBILE.md](MOBILE.md).
If you wish to create Android project yourself, there are a few kotlin files that are needed to run wry on Android and you have to set the following environment variables:
If you wish to create Android project yourself, there is a few requirements that your application needs to uphold:
1. You need to set a few environment variables that will be used to generate the necessary kotlin
files that you need to include in your Android application for wry to function properly:
- `WRY_ANDROID_PACKAGE`: which is the reversed domain name of your android project and the app name in snake_case, for example, `com.wry.example.wry_app`
- `WRY_ANDROID_LIBRARY`: for example, if your cargo project has a lib name `wry_app`, it will generate `libwry_app.so` so you se this env var to `wry_app`
- `WRY_ANDROID_KOTLIN_FILES_OUT_DIR`: for example, `path/to/app/src/main/kotlin/com/wry/example`
2. Your main Android Activity needs to inherit `AppCompatActivity`, preferably it should use the generated `WryActivity` or inherit it.
3. Your Rust app needs to call `wry::android_setup` function to setup the necessary logic to be able to create webviews later on.
4. Your Rust app needs to call `wry::android_binding!` macro to setup the JNI functions that will be called by `WryActivity` and various other places.
It is recommended to use [`tao`](https://docs.rs/tao/latest/tao/) crate as it provides maximum compatibility with `wry`
```rs
#[cfg(target_os = "android")]
{
tao::android_binding!(
com_example,
wry_app,
WryActivity,
wry::android_setup, // pass the wry::android_setup function to tao which will invoke when the event loop is created
_start_app
);
wry::android_binding!(com_example, ttt);
}
```
- `WRY_ANDROID_PACKAGE` which is the reversed domain name of your android project and the app name in snake_case for example: `com.wry.example.wry_app`
- `WRY_ANDROID_LIBRARY` for example: if your cargo project has a lib name `wry_app`, it will generate `libwry_app.so` so you se this env var to `wry_app`

View File

@@ -38,7 +38,7 @@ fn main() {
});
let kotlin_files_path =
PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("src/webview/android/kotlin");
PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("src/android/kotlin");
println!("cargo:rerun-if-changed={}", kotlin_files_path.display());
let kotlin_files = fs::read_dir(kotlin_files_path).expect("failed to read kotlin directory");

View File

@@ -0,0 +1,104 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::PathBuf;
use http::Request;
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
use wry::{
http::{header::CONTENT_TYPE, Response},
WebViewBuilder,
};
fn main() -> wry::Result<()> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let builder = WebViewBuilder::new(&window);
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let builder = {
use tao::platform::unix::WindowExtUnix;
let vbox = window.default_vbox().unwrap();
WebViewBuilder::new_gtk(vbox)
};
let _webview = builder
.with_asynchronous_custom_protocol("wry".into(), move |request, responder| {
match get_wry_response(request) {
Ok(http_response) => responder.respond(http_response),
Err(e) => responder.respond(
http::Response::builder()
.header(CONTENT_TYPE, "text/plain")
.status(500)
.body(e.to_string().as_bytes().to_vec())
.unwrap(),
),
}
})
// tell the webview to load the custom protocol
.with_url("wry://localhost")?
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
if let Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} = event
{
*control_flow = ControlFlow::Exit
}
});
}
fn get_wry_response(
request: Request<Vec<u8>>,
) -> Result<http::Response<Vec<u8>>, Box<dyn std::error::Error>> {
let path = request.uri().path();
// Read the file content from file path
let root = PathBuf::from("examples/custom_protocol");
let path = if path == "/" {
"index.html"
} else {
// removing leading slash
&path[1..]
};
let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?;
// Return asset contents and mime types based on file extentions
// If you don't want to do this manually, there are some crates for you.
// Such as `infer` and `mime_guess`.
let mimetype = if path.ends_with(".html") || path == "/" {
"text/html"
} else if path.ends_with(".js") {
"text/javascript"
} else if path.ends_with(".png") {
"image/png"
} else if path.ends_with(".wasm") {
"application/wasm"
} else {
unimplemented!();
};
Response::builder()
.header(CONTENT_TYPE, mimetype)
.body(content)
.map_err(Into::into)
}

View File

@@ -2,43 +2,53 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
fs::{canonicalize, read},
path::PathBuf,
};
use std::path::PathBuf;
use http::Request;
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
http::{header::CONTENT_TYPE, Response},
webview::WebViewBuilder,
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
use wry::{
http::{header::CONTENT_TYPE, Response},
WebViewBuilder,
};
const PAGE1_HTML: &[u8] = include_bytes!("custom_protocol_page1.html");
fn main() -> wry::Result<()> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Custom Protocol")
.build(&event_loop)
.unwrap();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let _webview = WebViewBuilder::new(window)
.unwrap()
.with_asynchronous_custom_protocol("wry".into(), move |request, responder| {
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let builder = WebViewBuilder::new(&window);
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let builder = {
use tao::platform::unix::WindowExtUnix;
let vbox = window.default_vbox().unwrap();
WebViewBuilder::new_gtk(vbox)
};
let _webview = builder
.with_custom_protocol("wry".into(), move |request| {
match get_wry_response(request) {
Ok(http_response) => responder.respond(http_response),
Err(e) => responder.respond(
http::Response::builder()
.header(CONTENT_TYPE, "text/plain")
.status(500)
.body(e.to_string().as_bytes().to_vec())
.unwrap(),
),
Ok(r) => r.map(Into::into),
Err(e) => http::Response::builder()
.header(CONTENT_TYPE, "text/plain")
.status(500)
.body(e.to_string().as_bytes().to_vec())
.unwrap()
.map(Into::into),
}
})
// tell the webview to load the custom protocol
@@ -48,13 +58,12 @@ fn main() -> wry::Result<()> {
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry application started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
if let Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} = event
{
*control_flow = ControlFlow::Exit
}
});
}
@@ -64,12 +73,14 @@ fn get_wry_response(
) -> Result<http::Response<Vec<u8>>, Box<dyn std::error::Error>> {
let path = request.uri().path();
// Read the file content from file path
let content = if path == "/" {
PAGE1_HTML.into()
let root = PathBuf::from("examples/custom_protocol");
let path = if path == "/" {
"index.html"
} else {
// `1..` for removing leading slash
read(canonicalize(PathBuf::from("examples").join(&path[1..]))?)?
// removing leading slash
&path[1..]
};
let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?;
// Return asset contents and mime types based on file extentions
// If you don't want to do this manually, there are some crates for you.

View File

@@ -0,0 +1,26 @@
<!-- Copyright 2020-2023 Tauri Programme within The Commons Conservancy
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: MIT -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Welcome to WRY!</h1>
<p>Page 1</p>
<textarea></textarea>
<p id="keypresses"></p>
<a href="/page2.html">Link</a>
<script type="text/javascript" src="/script.js"></script>
<script type="text/javascript">
document.body.addEventListener("keydown", (event) => {
document.querySelector("#keypresses").innerHTML += " " + event.key;
});
</script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
if (window.location.pathname.startsWith("/page2")) {
console.log("hello from javascript in page2");
} else {
console.log("hello from javascript in page1");
if (typeof WebAssembly.instantiateStreaming !== "undefined") {
WebAssembly.instantiateStreaming(fetch("/wasm.wasm")).then((wasm) => {
console.log(wasm.instance.exports.main()); // should log 42
});
} else {
// Older WKWebView may not support `WebAssembly.instantiateStreaming` yet.
fetch("/wasm.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes))
.then((wasm) => {
console.log(wasm.instance.exports.main()); // should log 42
});
}
}

View File

@@ -1,28 +0,0 @@
<!-- Copyright 2020-2023 Tauri Programme within The Commons Conservancy
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: MIT -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Welcome to WRY!</h1>
<p>Page 1</p>
<textarea></textarea>
<p id="keypresses"></p>
<a href="/custom_protocol_page2.html">Link</a>
<script type="text/javascript" src="/custom_protocol_script.js"></script>
<script type="text/javascript">
document.body.addEventListener('keydown', (event) => {
document.querySelector('#keypresses').innerHTML += ' ' + event.key
})
</script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
if (window.location.pathname.startsWith('/custom_protocol_page2')) {
console.log("hello from javascript in page2");
} else {
console.log("hello from javascript in page1");
if (typeof WebAssembly.instantiateStreaming !== 'undefined') {
WebAssembly.instantiateStreaming(fetch("/custom_protocol_wasm.wasm"))
.then(wasm => {
console.log(wasm.instance.exports.main()); // should log 42
});
} else {
// Older WKWebView may not support `WebAssembly.instantiateStreaming` yet.
fetch("/custom_protocol_wasm.wasm")
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes))
.then(wasm => {
console.log(wasm.instance.exports.main()); // should log 42
});
}
}

View File

@@ -2,20 +2,21 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tao::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::WindowBuilder,
};
use wry::WebViewBuilder;
enum UserEvent {
Minimize,
Maximize,
DragWindow,
CloseWindow,
}
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::{Window, WindowBuilder},
},
webview::WebViewBuilder,
};
enum UserEvent {
CloseWindow,
}
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
let window = WindowBuilder::new()
.with_decorations(false)
@@ -101,7 +102,6 @@ fn main() -> wry::Result<()> {
e.detail === 2
? window.ipc.postMessage('maximize')
: window.ipc.postMessage('drag_window');
}
})
document.addEventListener('touchstart', (e) => {
if (e.target.classList.contains('drag-region')) {
@@ -115,25 +115,44 @@ fn main() -> wry::Result<()> {
"#;
let proxy = event_loop.create_proxy();
let handler = move |window: &Window, req: String| {
if req == "minimize" {
window.set_minimized(true);
let handler = move |req: String| match req.as_str() {
"minimize" => {
let _ = proxy.send_event(UserEvent::Minimize);
}
if req == "maximize" {
window.set_maximized(!window.is_maximized());
"maximize" => {
let _ = proxy.send_event(UserEvent::Maximize);
}
if req == "close" {
"drag_window" => {
let _ = proxy.send_event(UserEvent::DragWindow);
}
"close" => {
let _ = proxy.send_event(UserEvent::CloseWindow);
}
if req == "drag_window" {
let _ = window.drag_window();
}
_ => {}
};
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let builder = WebViewBuilder::new(&window);
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let builder = {
use tao::platform::unix::WindowExtUnix;
let vbox = window.default_vbox().unwrap();
WebViewBuilder::new_gtk(vbox)
};
let mut webview = Some(
WebViewBuilder::new(window)
.unwrap()
builder
.with_html(HTML)?
.with_ipc_handler(handler)
.with_accept_first_mouse(true)
@@ -153,6 +172,13 @@ fn main() -> wry::Result<()> {
let _ = webview.take();
*control_flow = ControlFlow::Exit
}
Event::UserEvent(e) => match e {
UserEvent::Minimize => window.set_minimized(true),
UserEvent::Maximize => window.set_maximized(!window.is_maximized()),
UserEvent::DragWindow => window.drag_window().unwrap(),
UserEvent::CloseWindow => { /* handled above */ }
},
_ => (),
}
});

View File

@@ -1,140 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
const HTML: &str = r#"
</html>
<body>
<h2>ECMAScript support list:</h2>
<ul id="summary"></ul>
<table>
<thead>
<h3>Details:<h3>
</thead>
<tbody id="table"></tbody>
</table>
</body>
<script>
var featureSupport = [
{
version: "ECMAScript 5 (2009)",
features: [
{ name: "String.trim", supported: String.prototype.trim },
{ name: "Array.isArray", supported: Array.isArray },
{ name: "Array.forEach", supported: Array.prototype.forEach },
{ name: "Array.map", supported: Array.prototype.map },
{ name: "Array.filter", supported: Array.prototype.filter },
{ name: "Array.reduce", supported: Array.prototype.reduce },
{ name: "JSON.parse", supported: JSON.parse },
{ name: "Date.now", supported: Date.now }
]
},
{
version: "ECMAScript 6 (2015)",
features: [
{ name: "Array.find", supported: Array.prototype.find },
{ name: "Math.trunc", supported: Math.trunc },
{ name: "Number.isInteger", supported: Number.isInteger }
]
},
{
version: "ECMAScript 2016",
features: [
{ name: "Array.includes", supported: Array.prototype.includes }
]
},
{
version: "ECMAScript 2017",
features: [
{ name: "String.padStart", supported: String.prototype.padStart },
{ name: "String.padEnd", supported: String.prototype.padEnd },
{ name: "Object.entries", supported: Object.entries },
{ name: "Object.values", supported: Object.values },
]
},
{
version: "ECMAScript 2018",
features: [
{ name: "Promise.finally", supported: Promise.prototype.finally }
]
},
{
version: "ECMAScript 2019",
features: [
{ name: "Array.flat", supported: Array.prototype.flat },
{ name: "Array.flatMap", supported: Array.prototype.flatMap },
{ name: "Object.fromEntries", supported: Object.fromEntries },
{ name: "String.trimStart", supported: String.prototype.trimStart },
{ name: "String.trimEnd", supported: String.prototype.trimEnd },
{ name: "Function.toString", supported: Function.prototype.toString }
]
},
{
version: "ECMAScript 2020",
features: [
{ name: "Promise.allSettled", supported: Promise.allSettled },
{ name: "String.matchAll", supported: String.prototype.matchAll }
]
},
{
version: "ECMAScript 2021",
features: [
{ name: "String.replaceAll", supported: String.prototype.replaceAll },
{ name: "Promise.any", supported: Promise.any }
]
}
];
var tableElement = document.getElementById('table');
var summaryListElement = document.getElementById('summary');
for (var i = 0; i < featureSupport.length; i++) {
var versionDetails = featureSupport[i];
var versionSupported = true;
tableElement.innerHTML += `<tr> <td style="width: 200px; font-weight: bold">${versionDetails.version}</td> </tr>`
for (var j = 0; j < versionDetails.features.length; j++) {
var feature = versionDetails.features[j];
tableElement.innerHTML += `<tr> <td style="width: 200px">${feature.name}</td> <td>${feature.supported ? '✔' : '❌'} </td> </tr>`
if (!feature.supported) versionSupported = false;
}
summaryListElement.innerHTML += `<li> ${versionDetails.version}: ${versionSupported ? '✔' : '❌'}`
}
</script>
</html>
"#;
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Detect ECMAScript")
.build(&event_loop)
.unwrap();
let _webview = WebViewBuilder::new(window)
.unwrap()
.with_html(HTML)?
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry application started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}

View File

@@ -1,97 +0,0 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use std::{env::temp_dir, path::PathBuf};
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
const HTML: &str = r#"
<body>
<div>
<p> WRYYYYYYYYYYYYYYYYYYYYYY! </p>
<a download="allow.zip" href='https://github.com/tauri-apps/wry/archive/refs/tags/wry-v0.13.3.zip' id="link">Allowed Download</a>
<a download="deny.zip" href='https://github.com/tauri-apps/wry/archive/refs/tags/wry-v0.13.2.zip' id="link">Denied Download</a>
</div>
</body>
"#;
enum UserEvent {
DownloadStarted(String, String),
DownloadComplete(Option<PathBuf>, bool),
Rejected(String),
}
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
let proxy = event_loop.create_proxy();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_html(HTML)?
.with_download_started_handler({
let proxy = proxy.clone();
move |uri: String, default_path: &mut PathBuf| {
if uri.contains("wry-v0.13.3") {
let path = temp_dir().join("example.zip").as_path().to_path_buf();
*default_path = path.clone();
let submitted = proxy
.send_event(UserEvent::DownloadStarted(uri, path.display().to_string()))
.is_ok();
return submitted;
}
let _ = proxy.send_event(UserEvent::Rejected(uri));
false
}
})
.with_download_completed_handler({
let proxy = proxy;
move |_uri, path, success| {
let _ = proxy.send_event(UserEvent::DownloadComplete(path, success));
}
})
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::UserEvent(UserEvent::DownloadStarted(uri, temp_dir)) => {
println!("Download: {}", uri);
println!("Will write to: {:?}", temp_dir);
}
Event::UserEvent(UserEvent::DownloadComplete(path, success)) => {
let path = path.map(|_| temp_dir().join("example.zip"));
println!("Succeeded: {}", success);
if let Some(path) = path {
println!("Path: {}", path.to_string_lossy());
let metadata = path.metadata().unwrap();
println!("Size of {}Mb", (metadata.len() / 1024) / 1024)
} else {
println!("No output path")
}
}
Event::UserEvent(UserEvent::Rejected(uri)) => {
println!("Rejected download from: {}", uri)
}
_ => (),
}
});
}

View File

@@ -1,44 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
const HTML: &str = r#"data:text/html,
Drop files onto the window and read the console!<br>
Dropping files onto the following form is also possible:<br><br>
<input type="file"/>
"#;
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let _webview = WebViewBuilder::new(window)
.unwrap()
.with_url(HTML)?
.with_file_drop_handler(|_, data| {
println!("Window 1: {:?}", data);
false // Returning true will block the OS default behaviour.
})
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry application started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => {}
}
});
}

View File

@@ -1,89 +0,0 @@
// Copyright 2020-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::{Window, WindowBuilder},
},
webview::WebViewBuilder,
};
enum UserEvent {
ExecEval,
}
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
let proxy = event_loop.create_proxy();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)?;
let ipc_handler = move |_: &Window, req: String| {
if req == "exec-eval" {
let _ = proxy.send_event(UserEvent::ExecEval);
}
};
let _webview = WebViewBuilder::new(window)?
.with_html(
r#"
<button onclick="window.ipc.postMessage('exec-eval')">Exec eval</button>
"#,
)?
.with_ipc_handler(ipc_handler)
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::UserEvent(UserEvent::ExecEval) => {
// String
_webview
.evaluate_script_with_callback(
"if (!foo) { var foo = 'morbin'; } `${foo} time`",
|result| println!("String: {:?}", result),
)
.unwrap();
// Number
_webview
.evaluate_script_with_callback("var num = 9527; num", |result| {
println!("Number: {:?}", result)
})
.unwrap();
// Object
_webview
.evaluate_script_with_callback("var obj = { thank: 'you', '95': 27 }; obj", |result| {
println!("Object: {:?}", result)
})
.unwrap();
// Array
_webview
.evaluate_script_with_callback("var ary = [1,2,3,4,'5']; ary", |result| {
println!("Array: {:?}", result)
})
.unwrap();
// Exception thrown
_webview
.evaluate_script_with_callback("throw new Error()", |result| {
println!("Exception Occured: {:?}", result)
})
.unwrap();
}
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}

View File

@@ -1,31 +0,0 @@
<!-- Copyright 2020-2023 Tauri Programme within The Commons Conservancy
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: MIT -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Welcome to WRY!</h1>
<form action="#" method="POST">
<label for="fname">First name:</label><br />
<input type="text" id="fname" name="fname" value="John" /><br />
<label for="lname">Last name:</label><br />
<input type="text" id="lname" name="lname" value="Doe" /><br />
<label for="thumbnail">Thumbnail:</label><br />
<input type="file" id="thumbnail" name="thumbnail" value="Select your thumbnail image" /><br /><br />
<input type="submit" value="Submit" />
</form>
<p>
If you click the "Submit" button, the form-data will be sent to the custom
protocol.
</p>
</body>
</html>

View File

@@ -1,71 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
borrow::Cow,
fs::{canonicalize, read},
};
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
http::{header::CONTENT_TYPE, method::Method, Response},
webview::WebViewBuilder,
};
fn main() -> wry::Result<()> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)
.unwrap();
let _webview = WebViewBuilder::new(window)
.unwrap()
.with_custom_protocol("wry".into(), move |request| {
if request.method() == Method::POST {
let body_string = String::from_utf8_lossy(request.body());
for body in body_string.split('&') {
println!("Value sent; {:?}", body);
}
}
// remove leading slash
let path = &request.uri().path()[1..];
get_response(path).unwrap_or_else(|error| {
http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(CONTENT_TYPE, "text/plain")
.body(error.to_string().as_bytes().to_vec().into())
.unwrap()
})
})
// tell the webview to load the custom protocol
.with_url("wry://localhost/examples/form.html")?
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry application started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}
fn get_response(path: &str) -> Result<Response<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
Response::builder()
.header(CONTENT_TYPE, "text/html")
.body(read(canonicalize(path)?)?.into())
.map_err(Into::into)
}

View File

@@ -1,47 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Fullscreen, WindowBuilder},
},
webview::WebViewBuilder,
};
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("3D Render Test")
.with_fullscreen(Some(Fullscreen::Borderless(None)))
.build(&event_loop)
.unwrap();
let _webview = WebViewBuilder::new(window)
.unwrap()
.with_url("https://browserbench.org/MotionMark1.2/")?
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => {}
}
});
}
// Test Result:
// CPU: i7 9750H || GPU: Intel(R) UHD Graphics 630
// Linux kernel 5.8.18-18-ibryza-standard-xin
// Mesa Mesa 20.2.6
// ================================================
// Canvas score - Test 1: 542 - Test 2: 368
// WebGL score - Test 1: 1390 - Test 2: 1342
// Total score: 3642

View File

@@ -1,36 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_url("https://www.netflix.com/browse")?
// .with_incognito(true)
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}

View File

@@ -1,104 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use std::collections::HashMap;
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
window::{Window, WindowBuilder, WindowId},
},
webview::{WebView, WebViewBuilder},
};
enum UserEvent {
CloseWindow(WindowId),
NewWindow,
}
fn create_new_window(
title: String,
event_loop: &EventLoopWindowTarget<UserEvent>,
proxy: EventLoopProxy<UserEvent>,
) -> (WindowId, WebView) {
let window = WindowBuilder::new()
.with_title(title)
.build(event_loop)
.unwrap();
let window_id = window.id();
let handler = move |window: &Window, req: String| match req.as_str() {
"new-window" => {
let _ = proxy.send_event(UserEvent::NewWindow);
}
"close" => {
let _ = proxy.send_event(UserEvent::CloseWindow(window.id()));
}
_ if req.starts_with("change-title") => {
let title = req.replace("change-title:", "");
window.set_title(title.as_str());
}
_ => {}
};
let webview = WebViewBuilder::new(window)
.unwrap()
.with_html(
r#"
<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 = EventLoopBuilder::<UserEvent>::with_user_event().build();
let mut webviews = HashMap::new();
let proxy = event_loop.create_proxy();
let new_window = create_new_window(
format!("Window {}", webviews.len() + 1),
&event_loop,
proxy.clone(),
);
webviews.insert(new_window.0, new_window.1);
event_loop.run(move |event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry application started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
window_id,
..
} => {
webviews.remove(&window_id);
if webviews.is_empty() {
*control_flow = ControlFlow::Exit
}
}
Event::UserEvent(UserEvent::NewWindow) => {
let new_window = create_new_window(
format!("Window {}", webviews.len() + 1),
event_loop,
proxy.clone(),
);
webviews.insert(new_window.0, new_window.1);
}
Event::UserEvent(UserEvent::CloseWindow(id)) => {
webviews.remove(&id);
if webviews.is_empty() {
*control_flow = ControlFlow::Exit
}
}
_ => (),
}
});
}

106
examples/multiwebview.rs Normal file
View File

@@ -0,0 +1,106 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use winit::{
dpi::LogicalSize,
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
use wry::WebViewBuilder;
fn main() -> wry::Result<()> {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
{
use gtk::prelude::DisplayExtManual;
gtk::init()?;
if gtk::gdk::Display::default().unwrap().backend().is_wayland() {
panic!("This example doesn't support wayland!");
}
// we need to ignore this error here otherwise it will be catched by winit and will be
// make the example crash
winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| {
let error = error as *mut x11_dl::xlib::XErrorEvent;
(unsafe { (*error).error_code }) == 170
}));
}
let event_loop = EventLoop::new().unwrap();
let window = WindowBuilder::new()
.with_inner_size(LogicalSize::new(800, 800))
.build(&event_loop)
.unwrap();
let size = window.inner_size().to_logical::<u32>(window.scale_factor());
let webview = WebViewBuilder::new_as_child(&window)
.with_position((0, 0))
.with_size((size.width / 2, size.height / 2))
.with_url("https://tauri.app")?
.build()?;
let webview2 = WebViewBuilder::new_as_child(&window)
.with_position(((size.width / 2) as i32, 0))
.with_size((size.width / 2, size.height / 2))
.with_url("https://github.com/tauri-apps/wry")?
.build()?;
let webview3 = WebViewBuilder::new_as_child(&window)
.with_position((0, (size.height / 2) as i32))
.with_size((size.width / 2, size.height / 2))
.with_url("https://twitter.com/TauriApps")?
.build()?;
let webview4 = WebViewBuilder::new_as_child(&window)
.with_position(((size.width / 2) as i32, (size.height / 2) as i32))
.with_size((size.width / 2, size.height / 2))
.with_url("https://google.com")?
.build()?;
event_loop
.run(move |event, evl| {
evl.set_control_flow(ControlFlow::Poll);
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
while gtk::events_pending() {
gtk::main_iteration_do(false);
}
match event {
Event::WindowEvent {
event: WindowEvent::Resized(size),
..
} => {
let size = size.to_logical::<u32>(window.scale_factor());
webview.set_size((size.width / 2, size.height / 2));
webview.set_position((0, 0));
webview2.set_position(((size.width / 2) as i32, 0));
webview2.set_size((size.width / 2, size.height / 2));
webview3.set_position((0, (size.height / 2) as i32));
webview3.set_size((size.width / 2, size.height / 2));
webview4.set_position(((size.width / 2) as i32, (size.height / 2) as i32));
webview4.set_size((size.width / 2, size.height / 2));
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => evl.exit(),
_ => {}
}
})
.unwrap();
Ok(())
}

125
examples/multiwindow.rs Normal file
View File

@@ -0,0 +1,125 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::collections::HashMap;
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
window::{Window, WindowBuilder, WindowId},
};
use wry::{WebView, WebViewBuilder};
enum UserEvent {
CloseWindow(WindowId),
NewTitle(WindowId, String),
NewWindow,
}
fn main() -> wry::Result<()> {
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
let mut webviews = HashMap::new();
let proxy = event_loop.create_proxy();
let new_window = create_new_window(
format!("Window {}", webviews.len() + 1),
&event_loop,
proxy.clone(),
);
webviews.insert(new_window.0.id(), (new_window.0, new_window.1));
event_loop.run(move |event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
window_id,
..
} => {
webviews.remove(&window_id);
if webviews.is_empty() {
*control_flow = ControlFlow::Exit
}
}
Event::UserEvent(UserEvent::NewWindow) => {
let new_window = create_new_window(
format!("Window {}", webviews.len() + 1),
event_loop,
proxy.clone(),
);
webviews.insert(new_window.0.id(), (new_window.0, new_window.1));
}
Event::UserEvent(UserEvent::CloseWindow(id)) => {
webviews.remove(&id);
if webviews.is_empty() {
*control_flow = ControlFlow::Exit
}
}
Event::UserEvent(UserEvent::NewTitle(id, title)) => {
webviews.get(&id).unwrap().0.set_title(&title);
}
_ => (),
}
});
}
fn create_new_window(
title: String,
event_loop: &EventLoopWindowTarget<UserEvent>,
proxy: EventLoopProxy<UserEvent>,
) -> (Window, WebView) {
let window = WindowBuilder::new()
.with_title(title)
.build(event_loop)
.unwrap();
let window_id = window.id();
let handler = move |req: String| match req.as_str() {
"new-window" => {
let _ = proxy.send_event(UserEvent::NewWindow);
}
"close" => {
let _ = proxy.send_event(UserEvent::CloseWindow(window_id));
}
_ if req.starts_with("change-title") => {
let title = req.replace("change-title:", "");
let _ = proxy.send_event(UserEvent::NewTitle(window_id, title));
}
_ => {}
};
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let builder = WebViewBuilder::new(&window);
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let builder = {
use tao::platform::unix::WindowExtUnix;
let vbox = window.default_vbox().unwrap();
WebViewBuilder::new_gtk(vbox)
};
let webview = builder
.with_html(
r#"
<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, webview)
}

View File

@@ -1,48 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
enum UserEvent {
Navigation(String),
}
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
let proxy = event_loop.create_proxy();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_url("http://neverssl.com")?
.with_navigation_handler(move |uri: String| {
let submitted = proxy.send_event(UserEvent::Navigation(uri.clone())).is_ok();
submitted && uri.contains("neverssl")
})
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::UserEvent(UserEvent::Navigation(uri)) => {
println!("{}", uri);
}
_ => (),
}
});
}

View File

@@ -1,58 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
enum UserEvent {
NewWindow(String),
}
let html = r#"
<body>
<div>
<p> WRYYYYYYYYYYYYYYYYYYYYYY! </p>
<a href="https://www.wikipedia.org" target="_blank">Visit Wikipedia</a>
<a href="https://www.github.com" target="_blank">(Try to) visit GitHub</a>
</div>
</body>
"#;
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
let proxy = event_loop.create_proxy();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_html(html)?
.with_new_window_req_handler(move |uri: String| {
let submitted = proxy.send_event(UserEvent::NewWindow(uri.clone())).is_ok();
submitted && uri.contains("wikipedia")
})
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::UserEvent(UserEvent::NewWindow(uri)) => {
println!("New Window: {}", uri);
}
_ => (),
}
});
}

View File

@@ -1,44 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use wry::webview::{ProxyConfig, ProxyEndpoint};
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Proxy Test")
.build(&event_loop)?;
let http_proxy = ProxyConfig::Http(ProxyEndpoint {
host: "localhost".to_string(),
port: "3128".to_string(),
});
let _webview = WebViewBuilder::new(window)?
.with_proxy_config(http_proxy)
.with_url("https://www.myip.com/")?
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}

49
examples/simple.rs Normal file
View File

@@ -0,0 +1,49 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
use wry::WebViewBuilder;
fn main() -> wry::Result<()> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let builder = WebViewBuilder::new(&window);
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let builder = {
use tao::platform::unix::WindowExtUnix;
let vbox = window.default_vbox().unwrap();
WebViewBuilder::new_gtk(vbox)
};
let _webview = builder.with_url("https://tauri.app")?.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
if let Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} = event
{
*control_flow = ControlFlow::Exit
}
});
}

View File

@@ -1,31 +0,0 @@
<!-- Copyright 2020-2023 Tauri Programme within The Commons Conservancy
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: MIT -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<video id="video_source" style="width: 90vw; height: 90vh" controls="" autoplay="" name="media">
<source src="wry://localhost/examples/test_video.mp4" type="video/mp4" />
</video>
<script>
(function () {
if (navigator.userAgent.includes("Windows")) {
const video = document.getElementById("video_source");
const sources = video.getElementsByTagName("source");
sources[0].src = "http://wry.localhost/examples/test_video.mp4";
video.load();
}
})();
</script>
</body>
</html>

View File

@@ -1,158 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use http_range::HttpRange;
use std::{
borrow::Cow,
fs::{canonicalize, File},
io::{Read, Seek, SeekFrom},
path::PathBuf,
process::{Command, Stdio},
};
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
http::{header::CONTENT_TYPE, status::StatusCode, Response},
webview::WebViewBuilder,
};
fn main() -> wry::Result<()> {
let video_file = PathBuf::from("examples/test_video.mp4");
let video_url =
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
if !video_file.exists() {
// Downloading with curl this saves us from adding
// a Rust HTTP client dependency.
println!("Downloading {}", video_url);
let status = Command::new("curl")
.arg("-L")
.arg("-o")
.arg(&video_file)
.arg(video_url)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.unwrap();
assert!(status.status.success());
assert!(video_file.exists());
}
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)
.unwrap();
let _webview = WebViewBuilder::new(window)
.unwrap()
.with_custom_protocol("wry".into(), move |request| {
get_stream_response(request).unwrap_or_else(|error| {
http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(CONTENT_TYPE, "text/plain")
.body(error.to_string().as_bytes().to_vec().into())
.unwrap()
})
})
// tell the webview to load the custom protocol
.with_url("wry://localhost/examples/stream.html")?
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry application started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => {}
}
});
}
fn get_stream_response(
request: http::Request<Vec<u8>>,
) -> Result<http::Response<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
// remove leading slash
let path = &request.uri().path()[1..];
// Read the file content from file path
let mut content = File::open(canonicalize(path)?)?;
// Return asset contents and mime types based on file extentions
// If you don't want to do this manually, there are some crates for you.
// Such as `infer` and `mime_guess`.
let mut status_code = StatusCode::OK;
let mut buf = Vec::new();
// guess our mimetype from the path
let mimetype = if path.ends_with(".html") {
"text/html"
} else if path.ends_with(".mp4") {
"video/mp4"
} else {
unimplemented!();
};
// prepare our http response
let mut response = Response::builder();
// read our range header if it exist, so we can return partial content
if let Some(range) = request.headers().get("range") {
// Get the file size
let file_size = content.metadata().unwrap().len();
// we parse the range header
let range = HttpRange::parse(range.to_str().unwrap(), file_size).unwrap();
// let support only 1 range for now
let first_range = range.first();
if let Some(range) = first_range {
let mut real_length = range.length;
// prevent max_length;
// specially on webview2
if range.length > file_size / 3 {
// max size sent (400ko / request)
// as it's local file system we can afford to read more often
real_length = 1024 * 400;
}
// last byte we are reading, the length of the range include the last byte
// who should be skipped on the header
let last_byte = range.start + real_length - 1;
status_code = StatusCode::PARTIAL_CONTENT;
response = response.header("Connection", "Keep-Alive");
response = response.header("Accept-Ranges", "bytes");
// we need to overwrite our content length
response = response.header("Content-Length", real_length);
response = response.header(
"Content-Range",
format!("bytes {}-{}/{}", range.start, last_byte, file_size),
);
// seek our file bytes
content.seek(SeekFrom::Start(range.start))?;
content.take(real_length).read_to_end(&mut buf)?;
} else {
content.read_to_end(&mut buf)?;
}
} else {
content.read_to_end(&mut buf)?;
}
response
.header(CONTENT_TYPE, mimetype)
.status(status_code)
.body(buf.into())
.map_err(Into::into)
}

View File

@@ -2,53 +2,79 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
use wry::WebViewBuilder;
fn main() -> wry::Result<()> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
#[allow(unused_mut)]
let mut builder = WindowBuilder::new()
.with_decorations(false)
// There are actually three layer of background color when creating webview window.
// The first is window background...
.with_transparent(true)
.build(&event_loop)
.unwrap();
.with_transparent(true);
#[cfg(target_os = "windows")]
{
use tao::platform::windows::WindowBuilderExtWindows;
builder = builder.with_undecorated_shadow(false);
}
let window = builder.build(&event_loop).unwrap();
let _webview = WebViewBuilder::new(window)?
#[cfg(target_os = "windows")]
{
use tao::platform::windows::WindowExtWindows;
window.set_undecorated_shadow(true);
}
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let builder = WebViewBuilder::new(&window);
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let builder = {
use tao::platform::unix::WindowExtUnix;
let vbox = window.default_vbox().unwrap();
WebViewBuilder::new_gtk(vbox)
};
let _webview = builder
// The second is on webview...
.with_transparent(true)
// And the last is in html.
.with_html(
r#"
<!doctype html>
<html>
<body style="background-color:rgba(87,87,87,0.5);">hello</body>
<script>
window.onload = function() {
document.body.innerText = `hello, ${navigator.userAgent}`;
};
</script>
</html>"#,
r#"<html>
<body style="background-color:rgba(87,87,87,0.5);"></body>
<script>
window.onload = function() {
document.body.innerText = `hello, ${navigator.userAgent}`;
};
</script>
</html>"#,
)?
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => {}
if let Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} = event
{
*control_flow = ControlFlow::Exit
}
});
}

View File

@@ -1,45 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() -> wry::Result<()> {
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::{webview_version, WebViewBuilder},
};
let current_version = env!("CARGO_PKG_VERSION");
let current_webview_version = webview_version().unwrap();
let user_agent_string = format!(
"wry/{} ({}; {})",
current_version,
std::env::consts::OS,
current_webview_version
);
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Hello World")
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_user_agent(&user_agent_string)
.with_url("https://www.whatismybrowser.com/detect/what-is-my-user-agent")?
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}

218
examples/wgpu.rs Normal file
View File

@@ -0,0 +1,218 @@
use std::borrow::Cow;
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::Window,
};
use wry::WebViewBuilder;
async fn run(event_loop: EventLoop<()>, window: Window) {
let size = window.inner_size();
let instance = wgpu::Instance::default();
let surface = unsafe { instance.create_surface(&window) }.unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
// Request an adapter which can render to our surface
compatible_surface: Some(&surface),
})
.await
.expect("Failed to find an appropriate adapter");
// Create the logical device and command queue
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
},
None,
)
.await
.expect("Failed to create device");
// Load the shaders from disk
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(
r#"
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
let x = f32(i32(in_vertex_index) - 1);
let y = f32(i32(in_vertex_index & 1u) * 2 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
"#,
)),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let swapchain_capabilities = surface.get_capabilities(&adapter);
let swapchain_format = swapchain_capabilities.formats[0];
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(swapchain_format.into())],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});
let mut config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: swapchain_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: swapchain_capabilities.alpha_modes[0],
view_formats: vec![],
};
surface.configure(&device, &config);
let _webview = WebViewBuilder::new_as_child(&window)
.with_position((100, 100))
.with_size((400, 400))
.with_transparent(true)
.with_html(
r#"<html>
<body style="background-color:rgba(87,87,87,0.5);"></body>
<script>
window.onload = function() {
document.body.innerText = `hello, ${navigator.userAgent}`;
};
</script>
</html>"#,
)
.unwrap()
.build()
.unwrap();
event_loop
.run(move |event, evl| {
evl.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent {
event: WindowEvent::Resized(size),
..
} => {
// Reconfigure the surface with the new size
config.width = size.width;
config.height = size.height;
surface.configure(&device, &config);
// On macos the window needs to be redrawn manually after resizing
window.request_redraw();
}
Event::WindowEvent {
event: WindowEvent::RedrawRequested,
..
} => {
let frame = surface
.get_current_texture()
.expect("Failed to acquire next swap chain texture");
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(&render_pipeline);
rpass.draw(0..3, 0..1);
}
queue.submit(Some(encoder.finish()));
frame.present();
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => evl.exit(),
_ => {}
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
while gtk::events_pending() {
gtk::main_iteration_do(false);
}
})
.unwrap();
}
fn main() {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
{
use gtk::prelude::DisplayExtManual;
gtk::init().unwrap();
if gtk::gdk::Display::default().unwrap().backend().is_wayland() {
panic!("This example doesn't support wayland!");
}
// we need to ignore this error here otherwise it will be catched by winit and will be
// make the example crash
winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| {
let error = error as *mut x11_dl::xlib::XErrorEvent;
(unsafe { (*error).error_code }) == 170
}));
}
let event_loop = EventLoop::new().unwrap();
let window = winit::window::WindowBuilder::new()
.with_transparent(true)
.build(&event_loop)
.unwrap();
pollster::block_on(run(event_loop, window));
}

86
examples/winit.rs Normal file
View File

@@ -0,0 +1,86 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use winit::{
dpi::LogicalSize,
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
use wry::WebViewBuilder;
fn main() -> wry::Result<()> {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
{
use gtk::prelude::DisplayExtManual;
gtk::init().unwrap();
if gtk::gdk::Display::default().unwrap().backend().is_wayland() {
panic!("This example doesn't support wayland!");
}
// we need to ignore this error here otherwise it will be catched by winit and will be
// make the example crash
winit::platform::x11::register_xlib_error_hook(Box::new(|_display, error| {
let error = error as *mut x11_dl::xlib::XErrorEvent;
(unsafe { (*error).error_code }) == 170
}));
}
let event_loop = EventLoop::new().unwrap();
let window = WindowBuilder::new()
.with_inner_size(LogicalSize::new(800, 800))
.build(&event_loop)
.unwrap();
#[allow(unused_mut)]
let mut builder = WebViewBuilder::new(&window);
let _webview = builder.with_url("https://tauri.app")?.build()?;
event_loop
.run(move |event, evl| {
evl.set_control_flow(ControlFlow::Poll);
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
while gtk::events_pending() {
gtk::main_iteration_do(false);
}
match event {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
Event::WindowEvent {
event: WindowEvent::Resized(size),
..
} => {
_webview.set_size(size.into());
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => evl.exit(),
_ => {}
}
})
.unwrap();
Ok(())
}

View File

@@ -6,35 +6,30 @@ use http::{
header::{HeaderName, HeaderValue, CONTENT_TYPE},
Request,
};
pub use tao::platform::android::ndk_glue::jni::sys::{jboolean, jstring};
use tao::platform::android::ndk_glue::jni::{
errors::Error as JniError,
objects::{JClass, JMap, JObject, JString},
sys::jobject,
use jni::errors::Result as JniResult;
pub use jni::{
self,
objects::{GlobalRef, JClass, JMap, JObject, JString},
sys::{jboolean, jobject, jstring},
JNIEnv,
};
pub use ndk;
use super::{
ASSET_LOADER_DOMAIN, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER,
URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
};
use crate::webview::PageLoadEvent;
use crate::PageLoadEvent;
#[macro_export]
macro_rules! android_binding {
($domain:ident, $package:ident, $main:ident) => {
android_binding!($domain, $package, $main, ::wry)
($domain:ident, $package:ident) => {
::wry::android_binding!($domain, $package, ::wry)
};
($domain:ident, $package:ident, $main:ident, $wry:path) => {
use $wry::{
application::{
android_binding as tao_android_binding, android_fn, generate_package_name,
platform::android::ndk_glue::*,
},
webview::prelude::*,
};
tao_android_binding!($domain, $package, WryActivity, setup, $main);
($domain:ident, $package:ident, $wry:path) => {{
use $wry::prelude::*;
android_fn!(
$domain,
$package,
@@ -97,10 +92,10 @@ macro_rules! android_binding {
handleReceivedTitle,
[JObject, JString],
);
};
}};
}
fn handle_request(env: &mut JNIEnv, request: JObject) -> Result<jobject, JniError> {
fn handle_request(env: &mut JNIEnv, request: JObject) -> JniResult<jobject> {
if let Some(handler) = REQUEST_HANDLER.get() {
let mut request_builder = Request::builder();
@@ -113,10 +108,10 @@ fn handle_request(env: &mut JNIEnv, request: JObject) -> Result<jobject, JniErro
.into();
request_builder = request_builder.uri(&env.get_string(&url)?.to_string_lossy().to_string());
let method: JString = env
let method = env
.call_method(&request, "getMethod", "()Ljava/lang/String;", &[])?
.l()?
.into();
.l()
.map(JString::from)?;
request_builder = request_builder.method(
env
.get_string(&method)?
@@ -266,7 +261,7 @@ pub unsafe fn ipc(mut env: JNIEnv, _: JClass, arg: JString) {
Ok(arg) => {
let arg = arg.to_string_lossy().to_string();
if let Some(ipc) = IPC.get() {
(ipc.handler)(&ipc.window, arg)
(ipc.handler)(arg)
}
}
Err(e) => log::warn!("Failed to parse JString: {}", e),
@@ -279,7 +274,7 @@ pub unsafe fn handleReceivedTitle(mut env: JNIEnv, _: JClass, _webview: JObject,
Ok(title) => {
let title = title.to_string_lossy().to_string();
if let Some(title_handler) = TITLE_CHANGE_HANDLER.get() {
(title_handler.handler)(&title_handler.window, title)
(title_handler.handler)(title)
}
}
Err(e) => log::warn!("Failed to parse JString: {}", e),

View File

@@ -2,20 +2,17 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{webview::RGBA, Error};
use crate::{Error, RGBA};
use crossbeam_channel::*;
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JMap, JObject, JString},
JNIEnv,
};
use once_cell::sync::Lazy;
use std::os::unix::prelude::*;
use tao::platform::android::ndk_glue::{
jni::{
errors::Error as JniError,
objects::{GlobalRef, JObject, JString},
JNIEnv,
},
JMap, PACKAGE,
};
use super::find_class;
use super::{find_class, PACKAGE};
static CHANNEL: Lazy<(Sender<WebViewMessage>, Receiver<WebViewMessage>)> = Lazy::new(|| bounded(8));
pub static MAIN_PIPE: Lazy<[RawFd; 2]> = Lazy::new(|| {
@@ -39,7 +36,7 @@ impl<'a> MainPipe<'a> {
}
}
pub fn recv(&mut self) -> Result<(), JniError> {
pub fn recv(&mut self) -> JniResult<()> {
let activity = self.activity.as_obj();
if let Ok(message) = CHANNEL.1.recv() {
match message {
@@ -265,7 +262,7 @@ fn load_url<'a>(
url: &JString<'a>,
headers: Option<http::HeaderMap>,
main_thread: bool,
) -> Result<(), JniError> {
) -> JniResult<()> {
let function = if main_thread {
"loadUrlMainThread"
} else {
@@ -294,11 +291,7 @@ fn load_url<'a>(
Ok(())
}
fn load_html<'a>(
env: &mut JNIEnv<'a>,
webview: &JObject<'a>,
html: &JString<'a>,
) -> Result<(), JniError> {
fn load_html<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>, html: &JString<'a>) -> JniResult<()> {
env.call_method(
webview,
"loadHTMLMainThread",
@@ -312,7 +305,7 @@ fn set_background_color<'a>(
env: &mut JNIEnv<'a>,
webview: &JObject<'a>,
background_color: RGBA,
) -> Result<(), JniError> {
) -> JniResult<()> {
let color_class = env.find_class("android/graphics/Color")?;
let color = env.call_static_method(
color_class,
@@ -349,6 +342,6 @@ pub(crate) struct CreateWebViewAttributes {
pub background_color: Option<RGBA>,
pub headers: Option<http::HeaderMap>,
pub autoplay: bool,
pub on_webview_created: Option<Box<dyn Fn(super::Context) -> Result<(), JniError> + Send>>,
pub on_webview_created: Option<Box<dyn Fn(super::Context) -> JniResult<()> + Send>>,
pub user_agent: Option<String>,
}

View File

@@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
use super::{PageLoadEvent, WebContext, WebViewAttributes, RGBA};
use crate::{application::window::Window, webview::RequestAsyncResponder, Result};
use crate::{RequestAsyncResponder, Result};
use base64::{engine::general_purpose, Engine};
use crossbeam_channel::*;
use html5ever::{interface::QualName, namespace_url, ns, tendril::TendrilSink, LocalName};
@@ -11,19 +11,16 @@ use http::{
header::{HeaderValue, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
Request, Response as HttpResponse,
};
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JClass, JObject},
JNIEnv,
};
use kuchiki::NodeRef;
use ndk::looper::{FdEvent, ForeignLooper};
use once_cell::sync::OnceCell;
use sha2::{Digest, Sha256};
use std::{borrow::Cow, rc::Rc, sync::mpsc::channel};
use tao::platform::android::ndk_glue::{
jni::{
errors::Error as JniError,
objects::{GlobalRef, JClass, JObject},
JNIEnv,
},
ndk::looper::{FdEvent, ForeignLooper},
PACKAGE,
};
use std::{borrow::Cow, sync::mpsc::channel};
use url::Url;
pub(crate) mod binding;
@@ -55,9 +52,9 @@ macro_rules! define_static_handlers {
}
define_static_handlers! {
IPC = UnsafeIpc { handler: Box<dyn Fn(&Window, String)>, window: Rc<Window> };
IPC = UnsafeIpc { handler: Box<dyn Fn(String)> };
REQUEST_HANDLER = UnsafeRequestHandler { handler: Box<dyn Fn(Request<Vec<u8>>) -> Option<HttpResponse<Cow<'static, [u8]>>>> };
TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box<dyn Fn(&Window, String)>, window: Rc<Window> };
TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box<dyn Fn(String)> };
URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box<dyn Fn(String) -> bool> };
ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box<dyn Fn(PageLoadEvent, String)> };
}
@@ -65,7 +62,17 @@ define_static_handlers! {
pub static WITH_ASSET_LOADER: OnceCell<bool> = OnceCell::new();
pub static ASSET_LOADER_DOMAIN: OnceCell<String> = OnceCell::new();
pub unsafe fn setup(mut env: JNIEnv, looper: &ForeignLooper, activity: GlobalRef) {
pub(crate) static PACKAGE: OnceCell<String> = OnceCell::new();
/// Sets up the necessary logic for wry to be able to create the webviews later.
pub unsafe fn android_setup(
package: &str,
mut env: JNIEnv,
looper: &ForeignLooper,
activity: GlobalRef,
) {
PACKAGE.get_or_init(move || package.to_string());
// we must create the WebChromeClient here because it calls `registerForActivityResult`,
// which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook
let rust_webchrome_client_class = find_class(
@@ -103,14 +110,20 @@ pub unsafe fn setup(mut env: JNIEnv, looper: &ForeignLooper, activity: GlobalRef
.unwrap();
}
pub(crate) struct InnerWebView {
#[allow(unused)]
pub window: Rc<Window>,
}
pub(crate) struct InnerWebView;
impl InnerWebView {
pub fn new_as_child(
_window: &impl raw_window_handle::HasRawWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
_web_context: Option<&mut WebContext>,
) -> Result<Self> {
Self::new(_window, attributes, pl_attrs, _web_context)
}
pub fn new(
window: Rc<Window>,
_window: &impl raw_window_handle::HasRawWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
_web_context: Option<&mut WebContext>,
@@ -247,14 +260,12 @@ impl InnerWebView {
}))
});
let w = window.clone();
if let Some(i) = ipc_handler {
IPC.get_or_init(move || UnsafeIpc::new(Box::new(i), w));
IPC.get_or_init(move || UnsafeIpc::new(Box::new(i)));
}
let w = window.clone();
if let Some(i) = attributes.document_title_changed_handler {
TITLE_CHANGE_HANDLER.get_or_init(move || UnsafeTitleHandler::new(i, w));
TITLE_CHANGE_HANDLER.get_or_init(move || UnsafeTitleHandler::new(i));
}
if let Some(i) = attributes.navigation_handler {
@@ -265,7 +276,7 @@ impl InnerWebView {
ON_LOAD_HANDLER.get_or_init(move || UnsafeOnPageLoadHandler::new(h));
}
Ok(Self { window })
Ok(Self)
}
pub fn print(&self) {}
@@ -312,6 +323,22 @@ impl InnerWebView {
MainPipe::send(WebViewMessage::ClearAllBrowsingData);
Ok(())
}
pub fn set_position(&self, _position: (i32, i32)) {
// Unsupported.
}
pub fn set_size(&self, _size: (u32, u32)) {
// Unsupported.
}
pub fn set_visible(&self, _visible: bool) {
// Unsupported
}
pub fn focus(&self) {
// Unsupported
}
}
#[derive(Clone, Copy)]
@@ -359,7 +386,7 @@ pub fn find_class<'a>(
env: &mut JNIEnv<'a>,
activity: &JObject<'_>,
name: String,
) -> std::result::Result<JClass<'a>, JniError> {
) -> JniResult<JClass<'a>> {
let class_name = env.new_string(name.replace('/', "."))?;
let my_class = env
.call_method(

View File

@@ -1,16 +0,0 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Re-exported Tao APIs
//!
//! This module re-export [tao] APIs for user to create application windows. To learn more about
//! how to use tao, please see [its documentation](https://crates.io/crates/tao).
//!
//! [tao]: https://crates.io/crates/tao
#[cfg(feature = "tao")]
pub use tao::*;
#[cfg(feature = "winit")]
pub use winit::*;

91
src/error.rs Normal file
View File

@@ -0,0 +1,91 @@
/// Convenient type alias of Result type for wry.
pub type Result<T> = std::result::Result<T, Error>;
/// Errors returned by wry.
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[error(transparent)]
GlibError(#[from] gtk::glib::Error),
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[error(transparent)]
GlibBoolError(#[from] gtk::glib::BoolError),
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[error("Fail to fetch security manager")]
MissingManager,
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[error("Couldn't find X11 Display")]
X11DisplayNotFound,
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[error(transparent)]
XlibError(#[from] x11_dl::error::OpenError),
#[error("Failed to initialize the script")]
InitScriptError,
#[error("Bad RPC request: {0} ((1))")]
RpcScriptError(String, String),
#[error(transparent)]
NulError(#[from] std::ffi::NulError),
#[error(transparent)]
ReceiverError(#[from] std::sync::mpsc::RecvError),
#[error(transparent)]
SenderError(#[from] std::sync::mpsc::SendError<String>),
#[error("Failed to send the message")]
MessageSender,
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
UrlError(#[from] url::ParseError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[cfg(target_os = "windows")]
#[error("WebView2 error: {0}")]
WebView2Error(webview2_com::Error),
#[error("Duplicate custom protocol registered: {0}")]
DuplicateCustomProtocol(String),
#[error(transparent)]
HttpError(#[from] http::Error),
#[error("Infallible error, something went really wrong: {0}")]
Infallible(#[from] std::convert::Infallible),
#[cfg(target_os = "android")]
#[error(transparent)]
JniError(#[from] jni::errors::Error),
#[error("Failed to create proxy endpoint")]
ProxyEndpointCreationFailed,
#[error(transparent)]
WindowHandleError(#[from] raw_window_handle::HandleError),
#[error("the window handle kind is not supported")]
UnsupportedWindowHandle,
#[error(transparent)]
Utf8Error(#[from] std::str::Utf8Error),
}

1672
src/lib.rs

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
target_os = "netbsd",
target_os = "openbsd"
))]
use crate::webview::webkitgtk::WebContextImpl;
use crate::webkitgtk::WebContextImpl;
use std::path::{Path, PathBuf};
@@ -23,7 +23,7 @@ use std::path::{Path, PathBuf};
/// some actions like custom protocol on Mac. Please keep both instances when you still wish to
/// interact with them.
///
/// [`WebView`]: crate::webview::WebView
/// [`WebView`]: crate::WebView
#[derive(Debug)]
pub struct WebContext {
data: WebContextData,

View File

@@ -4,23 +4,15 @@
use std::{cell::Cell, path::PathBuf, rc::Rc};
use gtk::prelude::*;
use gtk::{glib, prelude::*};
use webkit2gtk::WebView;
use crate::{
application::{dpi::LogicalPosition, window::Window},
webview::FileDropEvent,
};
use crate::FileDropEvent;
pub(crate) fn connect_drag_event(
webview: Rc<WebView>,
window: Rc<Window>,
handler: Box<dyn Fn(&Window, FileDropEvent) -> bool>,
) {
pub(crate) fn connect_drag_event(webview: WebView, handler: Box<dyn Fn(FileDropEvent) -> bool>) {
let listener = Rc::new((handler, Cell::new(None)));
let listener_ref = listener.clone();
let w = window.clone();
webview.connect_drag_data_received(move |_, _, x, y, data, info, _| {
if info == 2 {
let uris = data
@@ -33,47 +25,33 @@ pub(crate) fn connect_drag_event(
.collect::<Vec<PathBuf>>();
listener_ref.1.set(Some(uris.clone()));
let scale_factor = w.scale_factor();
let position = LogicalPosition::new(x, y).to_physical(scale_factor);
listener_ref.0(
&w,
FileDropEvent::Hovered {
paths: uris,
position,
},
);
listener_ref.0(FileDropEvent::Hovered {
paths: uris,
position: (x, y),
});
} else {
// drag_data_received is called twice, so we can ignore this signal
}
});
let listener_ref = listener.clone();
let w = window.clone();
webview.connect_drag_drop(move |_, _, x, y, _| {
let uris = listener_ref.1.take();
if let Some(uris) = uris {
let scale_factor = w.scale_factor();
let position = LogicalPosition::new(x, y).to_physical(scale_factor);
listener_ref.0(
&w,
FileDropEvent::Dropped {
paths: uris,
position,
},
)
listener_ref.0(FileDropEvent::Dropped {
paths: uris,
position: (x, y),
})
} else {
false
}
});
let listener_ref = listener.clone();
let w = window.clone();
webview.connect_drag_leave(move |_, _, time| {
if time == 0 {
// The user cancelled the drag n drop
listener_ref.0(&w, FileDropEvent::Cancelled);
listener_ref.0(FileDropEvent::Cancelled);
} else {
// The user dropped the file on the window, but this will be handled in connect_drag_drop instead
}
@@ -81,10 +59,9 @@ pub(crate) fn connect_drag_event(
// Called when a drag "fails" - we'll just emit a Cancelled event.
let listener_ref = listener.clone();
let w = window;
webview.connect_drag_failed(move |_, _, _| {
if listener_ref.0(&w, FileDropEvent::Cancelled) {
gtk::glib::Propagation::Stop
if listener_ref.0(FileDropEvent::Cancelled) {
glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}

View File

@@ -2,15 +2,20 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use gtk::{gdk::EventMask, gio::Cancellable, prelude::*};
use gdkx11::{
glib::translate::{FromGlibPtrFull, ToGlibPtr},
X11Display,
};
use gtk::{
gdk::{self, EventMask},
gio::Cancellable,
prelude::*,
};
use javascriptcore::ValueExt;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
#[cfg(any(debug_assertions, feature = "devtools"))]
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
rc::Rc,
sync::{Arc, Mutex},
};
use std::sync::{Arc, Mutex};
use url::Url;
use webkit2gtk::{
AutoplayPolicy, InputMethodContextExt, LoadEvent, NavigationPolicyDecision,
@@ -23,39 +28,156 @@ use webkit2gtk_sys::{
webkit_get_major_version, webkit_get_micro_version, webkit_get_minor_version,
webkit_policy_decision_ignore, webkit_policy_decision_use,
};
use x11_dl::xlib::*;
use web_context::WebContextExt;
pub use web_context::WebContextImpl;
use crate::{
application::{platform::unix::*, window::Window},
webview::{proxy::ProxyConfig, web_context::WebContext, PageLoadEvent, WebViewAttributes, RGBA},
Error, Result,
proxy::ProxyConfig, web_context::WebContext, Error, PageLoadEvent, Result, WebViewAttributes,
RGBA,
};
mod file_drop;
mod synthetic_mouse_events;
mod undecorated_resizing;
mod web_context;
use javascriptcore::ValueExt;
pub(crate) struct InnerWebView {
pub webview: Rc<WebView>,
pub webview: WebView,
#[cfg(any(debug_assertions, feature = "devtools"))]
is_inspector_open: Arc<AtomicBool>,
pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
is_child: bool,
xlib: Option<Xlib>,
x11_display: Option<*mut std::ffi::c_void>,
x11_window: Option<u64>,
display: Option<gdk::Display>,
gtk_window: Option<gtk::Window>,
}
impl Drop for InnerWebView {
fn drop(&mut self) {
unsafe { self.webview.destroy() }
if let Some(xlib) = &self.xlib {
if self.is_child {
unsafe { (xlib.XDestroyWindow)(self.x11_display.unwrap() as _, self.x11_window.unwrap()) };
}
}
if let Some(window) = &self.gtk_window {
window.close();
}
}
}
impl InnerWebView {
pub fn new(
window: Rc<Window>,
pub fn new<W: HasRawWindowHandle>(
window: &W,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
web_context: Option<&mut WebContext>,
) -> Result<Self> {
Self::new_x11(window, attributes, pl_attrs, web_context, false)
}
pub fn new_as_child<W: HasRawWindowHandle>(
parent: &W,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
web_context: Option<&mut WebContext>,
) -> Result<Self> {
Self::new_x11(parent, attributes, pl_attrs, web_context, true)
}
fn new_x11<W: HasRawWindowHandle>(
window: &W,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
web_context: Option<&mut WebContext>,
is_child: bool,
) -> Result<Self> {
let xlib = Xlib::open()?;
let window_handle = match window.raw_window_handle() {
RawWindowHandle::Xlib(w) => w.window,
_ => return Err(Error::UnsupportedWindowHandle),
};
let gdk_display = gdk::Display::default().ok_or(Error::X11DisplayNotFound)?;
let gx11_display: &X11Display = gdk_display.downcast_ref().unwrap();
let raw = gx11_display.to_glib_none().0;
let display = unsafe { gdkx11::ffi::gdk_x11_display_get_xdisplay(raw) };
let window = if is_child {
let child = unsafe {
(xlib.XCreateSimpleWindow)(
display as _,
window_handle,
attributes.position.map(|p| p.0).unwrap_or(0),
attributes.position.map(|p| p.1).unwrap_or(0),
attributes.size.map(|s| s.0).unwrap_or(0),
attributes.size.map(|s| s.1).unwrap_or(0),
0,
0,
0,
)
};
if attributes.visible {
unsafe { (xlib.XMapWindow)(display as _, child) };
}
child
} else {
window_handle
};
let gdk_window = unsafe {
let raw = gdkx11::ffi::gdk_x11_window_foreign_new_for_display(raw, window);
gdk::Window::from_glib_full(raw)
};
let gtk_window = gtk::Window::new(gtk::WindowType::Toplevel);
gtk_window.connect_realize(move |widget| widget.set_window(gdk_window.clone()));
gtk_window.set_has_window(true);
gtk_window.realize();
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
gtk_window.add(&vbox);
let hidden = !attributes.visible;
Self::new_gtk(&vbox, attributes, pl_attrs, web_context).map(|mut w| {
// for some reason, if the webview starts as hidden,
// we will need about 3 calls to `webview.set_visible`
// with alternating value.
// calling gtk_window.show_all() then hiding it again
// seems to fix the issue.
gtk_window.show_all();
if hidden {
gtk_window.hide();
}
w.is_child = is_child;
w.xlib = Some(xlib);
w.display = Some(gdk_display);
w.x11_display = Some(display as _);
w.x11_window = Some(window);
w.gtk_window = Some(gtk_window);
w
})
}
pub fn new_gtk<W>(
container: &W,
mut attributes: WebViewAttributes,
_pl_attrs: super::PlatformSpecificWebViewAttributes,
web_context: Option<&mut WebContext>,
) -> Result<Self> {
let window_rc = Rc::clone(&window);
let window = &window.gtk_window();
) -> Result<Self>
where
W: IsA<gtk::Container>,
{
let window_id = container.as_ptr() as isize;
// default_context allows us to create a scoped context on-demand
let mut default_context;
@@ -108,44 +230,25 @@ impl InnerWebView {
web_context.register_automation(webview.clone());
// Message handler
let webview = Rc::new(webview);
let w = window_rc.clone();
let ipc_handler = attributes.ipc_handler.take();
let manager = web_context.manager();
// Use the window hash as the script handler name to prevent from conflict when sharing same
// web context.
let window_hash = {
let mut hasher = DefaultHasher::new();
w.id().hash(&mut hasher);
hasher.finish().to_string()
};
// Connect before registering as recommended by the docs
manager.connect_script_message_received(None, move |_m, msg| {
if let Some(js) = msg.js_value() {
if let Some(ipc_handler) = &ipc_handler {
ipc_handler(&w, js.to_string());
ipc_handler(js.to_string());
}
}
});
// Register the handler we just connected
manager.register_script_message_handler(&window_hash);
// Allow the webview to close it's own window
let close_window = window_rc.clone();
webview.connect_close(move |_| {
close_window.gtk_window().close();
});
manager.register_script_message_handler(&window_id.to_string());
// document title changed handler
if let Some(document_title_changed_handler) = attributes.document_title_changed_handler {
let w = window_rc.clone();
webview.connect_title_notify(move |webview| {
document_title_changed_handler(
&w,
webview.title().map(|t| t.to_string()).unwrap_or_default(),
)
document_title_changed_handler(webview.title().map(|t| t.to_string()).unwrap_or_default())
});
}
@@ -174,7 +277,6 @@ impl InnerWebView {
);
synthetic_mouse_events::setup(&webview);
undecorated_resizing::setup(&webview);
if attributes.navigation_handler.is_some() || attributes.new_window_req_handler.is_some() {
webview.connect_decide_policy(move |_webview, policy_decision, policy_type| {
@@ -216,24 +318,30 @@ impl InnerWebView {
)
}
// tao adds a default vertical box so we check for that first
if let Some(vbox) = window_rc.default_vbox() {
vbox.pack_start(&*webview, true, true, 0);
if container.type_().name() == "GtkBox" {
container
.dynamic_cast_ref::<gtk::Box>()
.unwrap()
.pack_start(&webview, true, true, 0);
} else {
window.add(&*webview);
container.add(&webview);
}
if attributes.visible {
webview.show_all();
}
if attributes.focused {
webview.grab_focus();
}
if let Some(context) = WebViewExt::context(&*webview) {
if let Some(context) = WebViewExt::context(&webview) {
use webkit2gtk::WebContextExt;
context.set_use_system_appearance_for_scrollbars(false);
}
// Enable webgl, webaudio, canvas features as default.
if let Some(settings) = WebViewExt::settings(&*webview) {
if let Some(settings) = WebViewExt::settings(&webview) {
settings.set_enable_webgl(true);
settings.set_enable_webaudio(true);
settings
@@ -273,17 +381,13 @@ impl InnerWebView {
// File drop handling
if let Some(file_drop_handler) = attributes.file_drop_handler {
file_drop::connect_drag_event(webview.clone(), window_rc, file_drop_handler);
}
if window.get_visible() {
window.show_all();
file_drop::connect_drag_event(webview.clone(), file_drop_handler);
}
#[cfg(any(debug_assertions, feature = "devtools"))]
let is_inspector_open = {
let is_inspector_open = Arc::new(AtomicBool::default());
if let Some(inspector) = WebViewExt::inspector(&*webview) {
if let Some(inspector) = WebViewExt::inspector(&webview) {
let is_inspector_open_ = is_inspector_open.clone();
inspector.connect_bring_to_front(move |_| {
is_inspector_open_.store(true, Ordering::Relaxed);
@@ -302,12 +406,18 @@ impl InnerWebView {
#[cfg(any(debug_assertions, feature = "devtools"))]
is_inspector_open,
pending_scripts: Arc::new(Mutex::new(Some(Vec::new()))),
is_child: false,
xlib: None,
display: None,
x11_display: None,
x11_window: None,
gtk_window: None,
};
// Initialize message handler
let mut init = String::with_capacity(115 + 20 + 22);
init.push_str("Object.defineProperty(window, 'ipc', {value: Object.freeze({postMessage:function(x){window.webkit.messageHandlers[\"");
init.push_str(&window_hash);
init.push_str(&window_id.to_string());
init.push_str("\"].postMessage(x)}})})");
w.init(&init)?;
@@ -328,7 +438,7 @@ impl InnerWebView {
// Navigation
if let Some(url) = attributes.url {
web_context.queue_load_uri(Rc::clone(&w.webview), url, attributes.headers);
web_context.queue_load_uri(w.webview.clone(), url, attributes.headers);
web_context.flush_queue_loader();
} else if let Some(html) = attributes.html {
w.webview.load_html(&html, None);
@@ -418,7 +528,7 @@ impl InnerWebView {
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn open_devtools(&self) {
if let Some(inspector) = WebViewExt::inspector(&*self.webview) {
if let Some(inspector) = WebViewExt::inspector(&self.webview) {
inspector.show();
// `bring-to-front` is not received in this case
self.is_inspector_open.store(true, Ordering::Relaxed);
@@ -427,7 +537,7 @@ impl InnerWebView {
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn close_devtools(&self) {
if let Some(inspector) = WebViewExt::inspector(&*self.webview) {
if let Some(inspector) = WebViewExt::inspector(&self.webview) {
inspector.close();
}
}
@@ -438,7 +548,7 @@ impl InnerWebView {
}
pub fn zoom(&self, scale_factor: f64) {
WebViewExt::set_zoom_level(&*self.webview, scale_factor);
WebViewExt::set_zoom_level(&self.webview, scale_factor);
}
pub fn set_background_color(&self, background_color: RGBA) -> Result<()> {
@@ -471,8 +581,8 @@ impl InnerWebView {
}
pub fn clear_all_browsing_data(&self) -> Result<()> {
if let Some(context) = WebViewExt::context(&*self.webview) {
use webkit2gtk::WebContextExt;
use webkit2gtk::WebContextExt;
if let Some(context) = WebViewExt::context(&self.webview) {
if let Some(data_manger) = context.website_data_manager() {
webkit2gtk::WebsiteDataManagerExtManual::clear(
&data_manger,
@@ -486,6 +596,52 @@ impl InnerWebView {
Ok(())
}
pub fn set_position(&self, position: (i32, i32)) {
if self.is_child {
if let Some(window) = &self.gtk_window {
window.move_(position.0, position.1);
}
}
}
pub fn set_size(&self, size: (u32, u32)) {
if let Some(window) = &self.gtk_window {
if self.is_child {
window.window().unwrap().resize(size.0 as _, size.1 as _);
}
window.size_allocate(&gtk::Allocation::new(200, 200, size.0 as _, size.1 as _));
}
}
pub fn set_visible(&self, visible: bool) {
if self.is_child {
let xlib = self.xlib.as_ref().unwrap();
if visible {
unsafe { (xlib.XMapWindow)(self.x11_display.unwrap() as _, self.x11_window.unwrap()) };
} else {
unsafe { (xlib.XUnmapWindow)(self.x11_display.unwrap() as _, self.x11_window.unwrap()) };
}
}
if visible {
self.webview.show_all();
} else {
self.webview.hide();
}
if let Some(window) = &self.gtk_window {
if visible {
window.show_all();
} else {
window.hide();
}
}
}
pub fn focus(&self) {
self.webview.grab_focus();
}
}
pub fn platform_webview_version() -> Result<String> {

View File

@@ -4,11 +4,8 @@
//! Unix platform extensions for [`WebContext`](super::WebContext).
use crate::{
webview::{web_context::WebContextData, RequestAsyncResponder},
Error,
};
use gtk::glib::FileError;
use crate::{web_context::WebContextData, Error, RequestAsyncResponder};
use gtk::glib;
use http::{header::CONTENT_TYPE, Request, Response as HttpResponse};
use std::{
borrow::Cow,
@@ -136,7 +133,7 @@ pub trait WebContextExt {
/// Add a [`WebView`] to the queue waiting to be opened.
///
/// See the `WebviewUriLoader` for more information.
fn queue_load_uri(&self, webview: Rc<WebView>, url: Url, headers: Option<http::HeaderMap>);
fn queue_load_uri(&self, webview: WebView, url: Url, headers: Option<http::HeaderMap>);
/// Flush all queued [`WebView`]s waiting to load a uri.
///
@@ -189,7 +186,7 @@ impl WebContextExt for super::WebContext {
}
}
fn queue_load_uri(&self, webview: Rc<WebView>, url: Url, headers: Option<http::HeaderMap>) {
fn queue_load_uri(&self, webview: WebView, url: Url, headers: Option<http::HeaderMap>) {
self.os.webview_uri_loader.push(webview, url, headers)
}
@@ -328,11 +325,7 @@ where
#[cfg(feature = "linux-body")]
{
use gtk::{
gdk::prelude::{InputStreamExt, InputStreamExtManual},
gio::Cancellable,
glib::Bytes,
};
use gtk::{gdk::prelude::InputStreamExtManual, gio::Cancellable};
// Set request http body
let cancellable: Option<&Cancellable> = None;
@@ -354,7 +347,7 @@ where
}
result
})
.unwrap_or(Vec::new());
.unwrap_or_default();
}
#[cfg(not(feature = "linux-body"))]
{
@@ -366,7 +359,7 @@ where
Err(_) => {
request.finish_error(&mut gtk::glib::Error::new(
// TODO: use UriError when we can use 2_66 webkit2gtk feature flag
FileError::Exist,
glib::FileError::Exist,
"Could not get uri.",
));
return;
@@ -402,8 +395,8 @@ where
handler(http_request, RequestAsyncResponder { responder });
} else {
request.finish_error(&mut gtk::glib::Error::new(
FileError::Exist,
request.finish_error(&mut glib::Error::new(
glib::FileError::Exist,
"Could not get uri.",
));
}
@@ -443,7 +436,7 @@ where
#[derive(Debug, Default)]
struct WebviewUriLoader {
lock: AtomicBool,
queue: Mutex<VecDeque<(Rc<WebView>, Url, Option<http::HeaderMap>)>>,
queue: Mutex<VecDeque<(WebView, Url, Option<http::HeaderMap>)>>,
}
impl WebviewUriLoader {
@@ -458,13 +451,13 @@ impl WebviewUriLoader {
}
/// Add a [`WebView`] to the queue.
fn push(&self, webview: Rc<WebView>, url: Url, headers: Option<http::HeaderMap>) {
fn push(&self, webview: WebView, url: Url, headers: Option<http::HeaderMap>) {
let mut queue = self.queue.lock().expect("poisoned load queue");
queue.push_back((webview, url, headers))
}
/// Remove a [`WebView`] from the queue and return it.
fn pop(&self) -> Option<(Rc<WebView>, Url, Option<http::HeaderMap>)> {
fn pop(&self) -> Option<(WebView, Url, Option<http::HeaderMap>)> {
let mut queue = self.queue.lock().expect("poisoned load queue");
queue.pop_front()
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,103 +0,0 @@
use crate::application::platform::unix::*;
use gtk::{
gdk::{Cursor, WindowEdge},
prelude::*,
};
use webkit2gtk::WebView;
pub fn setup(webview: &WebView) {
webview.connect_motion_notify_event(|webview, event| {
// This one should be GtkWindow
if let Some(widget) = webview.parent() {
// This one should be GtkWindow
if let Some(window) = widget.parent() {
// Safe to unwrap unless this is not from tao
let window: gtk::Window = window.downcast().unwrap();
if !window.is_decorated() && window.is_resizable() {
if let Some(window) = window.window() {
let (cx, cy) = event.root();
let edge = hit_test(&window, cx, cy);
// FIXME: calling `window.begin_resize_drag` seems to revert the cursor back to normal style
window.set_cursor(
Cursor::from_name(
&window.display(),
match edge {
WindowEdge::North => "n-resize",
WindowEdge::South => "s-resize",
WindowEdge::East => "e-resize",
WindowEdge::West => "w-resize",
WindowEdge::NorthWest => "nw-resize",
WindowEdge::NorthEast => "ne-resize",
WindowEdge::SouthEast => "se-resize",
WindowEdge::SouthWest => "sw-resize",
_ => "default",
},
)
.as_ref(),
);
}
}
}
}
gtk::glib::Propagation::Proceed
});
webview.connect_button_press_event(move |webview, event| {
if event.button() == 1 {
let (cx, cy) = event.root();
// This one should be GtkBox
if let Some(widget) = webview.parent() {
// This one should be GtkWindow
if let Some(window) = widget.parent() {
// Safe to unwrap unless this is not from tao
let window: gtk::Window = window.downcast().unwrap();
if !window.is_decorated() && window.is_resizable() {
if let Some(window) = window.window() {
// Safe to unwrap since it's a valid GtkWindow
let result = hit_test(&window, cx, cy);
// we ignore the `__Unknown` variant so the webview receives the click correctly if it is not on the edges.
match result {
WindowEdge::__Unknown(_) => (),
_ => window.begin_resize_drag(result, 1, cx as i32, cy as i32, event.time()),
}
}
}
}
}
}
gtk::glib::Propagation::Proceed
});
webview.connect_touch_event(|webview, event| {
// This one should be GtkBox
if let Some(widget) = webview.parent() {
// This one should be GtkWindow
if let Some(window) = widget.parent() {
// Safe to unwrap unless this is not from tao
let window: gtk::Window = window.downcast().unwrap();
if !window.is_decorated() && window.is_resizable() && !window.is_maximized() {
if let Some(window) = window.window() {
if let Some((cx, cy)) = event.root_coords() {
if let Some(device) = event.device() {
let result = hit_test(&window, cx, cy);
// we ignore the `__Unknown` variant so the window receives the click correctly if it is not on the edges.
match result {
WindowEdge::__Unknown(_) => (),
_ => window.begin_resize_drag_for_device(
result,
&device,
0,
cx as i32,
cy as i32,
event.time(),
),
}
}
}
}
}
}
}
gtk::glib::Propagation::Proceed
});
}

View File

@@ -1,155 +0,0 @@
#![allow(non_snake_case)]
use once_cell::sync::Lazy;
use windows::{
core::HRESULT,
Win32::{
Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM},
Graphics::Gdi::{
GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST,
},
UI::{
HiDpi::{MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE},
Input::KeyboardAndMouse::ReleaseCapture,
WindowsAndMessaging::{
GetWindowRect, IsProcessDPIAware, PostMessageW, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT,
HTCLIENT, HTLEFT, HTNOWHERE, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT,
},
},
},
};
use super::get_function;
#[inline]
pub fn MAKELPARAM(x: i16, y: i16) -> LPARAM {
LPARAM(((x as u16 as u32) | ((y as u16 as u32) << 16)) as usize as _)
}
#[inline]
pub fn begin_resize_drag(
hwnd: isize,
edge: isize,
button: u32,
x: i32,
y: i32,
) -> windows::core::Result<()> {
unsafe {
let w_param = WPARAM(edge as _);
let l_param = MAKELPARAM(x as i16, y as i16);
ReleaseCapture()?;
PostMessageW(HWND(hwnd), button, w_param, l_param)
}
}
type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32;
type GetDpiForMonitor = unsafe extern "system" fn(
hmonitor: HMONITOR,
dpi_type: MONITOR_DPI_TYPE,
dpi_x: *mut u32,
dpi_y: *mut u32,
) -> HRESULT;
static GET_DPI_FOR_WINDOW: Lazy<Option<GetDpiForWindow>> =
Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));
static GET_DPI_FOR_MONITOR: Lazy<Option<GetDpiForMonitor>> =
Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor));
const BASE_DPI: u32 = 96;
fn dpi_to_scale_factor(dpi: u32) -> f64 {
dpi as f64 / BASE_DPI as f64
}
unsafe fn hwnd_dpi(hwnd: HWND) -> u32 {
let hdc = GetDC(hwnd);
if hdc.is_invalid() {
panic!("[tao] `GetDC` returned null!");
}
if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW {
// We are on Windows 10 Anniversary Update (1607) or later.
match GetDpiForWindow(hwnd) {
0 => BASE_DPI, // 0 is returned if hwnd is invalid
dpi => dpi as u32,
}
} else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR {
// We are on Windows 8.1 or later.
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if monitor.is_invalid() {
return BASE_DPI;
}
let mut dpi_x = 0;
let mut dpi_y = 0;
if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y).is_ok() {
dpi_x as u32
} else {
BASE_DPI
}
} else {
// We are on Vista or later.
if IsProcessDPIAware().as_bool() {
// If the process is DPI aware, then scaling must be handled by the application using
// this DPI value.
GetDeviceCaps(hdc, LOGPIXELSX) as u32
} else {
// If the process is DPI unaware, then scaling is performed by the OS; we thus return
// 96 (scale factor 1.0) to prevent the window from being re-scaled by both the
// application and the WM.
BASE_DPI
}
}
}
const BORDERLESS_RESIZE_INSET: i32 = 5;
pub fn hit_test(hwnd: isize, cx: i32, cy: i32) -> LRESULT {
let hwnd = HWND(hwnd);
let mut window_rect = RECT::default();
unsafe {
if GetWindowRect(hwnd, &mut window_rect).is_ok() {
const CLIENT: isize = 0b0000;
const LEFT: isize = 0b0001;
const RIGHT: isize = 0b0010;
const TOP: isize = 0b0100;
const BOTTOM: isize = 0b1000;
const TOPLEFT: isize = TOP | LEFT;
const TOPRIGHT: isize = TOP | RIGHT;
const BOTTOMLEFT: isize = BOTTOM | LEFT;
const BOTTOMRIGHT: isize = BOTTOM | RIGHT;
let RECT {
left,
right,
bottom,
top,
} = window_rect;
let dpi = hwnd_dpi(hwnd);
let scale_factor = dpi_to_scale_factor(dpi);
let inset = (BORDERLESS_RESIZE_INSET as f64 * scale_factor) as i32;
#[rustfmt::skip]
let result =
(LEFT * (if cx < (left + inset) { 1 } else { 0 }))
| (RIGHT * (if cx >= (right - inset) { 1 } else { 0 }))
| (TOP * (if cy < (top + inset) { 1 } else { 0 }))
| (BOTTOM * (if cy >= (bottom - inset) { 1 } else { 0 }));
LRESULT(match result {
CLIENT => HTCLIENT,
LEFT => HTLEFT,
RIGHT => HTRIGHT,
TOP => HTTOP,
BOTTOM => HTBOTTOM,
TOPLEFT => HTTOPLEFT,
TOPRIGHT => HTTOPRIGHT,
BOTTOMLEFT => HTBOTTOMLEFT,
BOTTOMRIGHT => HTBOTTOMRIGHT,
_ => HTNOWHERE,
} as _)
} else {
LRESULT(HTNOWHERE as _)
}
}
}

View File

@@ -2,12 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::webview::FileDropEvent;
// A silly implementation of file drop handling for Windows!
// This can be pretty much entirely replaced when WebView2 SDK 1.0.721-prerelease becomes stable.
// https://docs.microsoft.com/en-us/microsoft-edge/webview2/releasenotes#10721-prerelease
// https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2experimentalcompositioncontroller3?view=webview2-1.0.721-prerelease&preserve-view=true
use crate::FileDropEvent;
use std::{
cell::UnsafeCell,
@@ -37,90 +34,57 @@ use windows::Win32::{
use windows_implement::implement;
use crate::application::{
dpi::PhysicalPosition, platform::windows::WindowExtWindows, window::Window,
};
#[derive(Default)]
pub(crate) struct FileDropController {
drop_targets: Vec<IDropTarget>,
}
impl FileDropController {
pub(crate) fn new() -> Self {
FileDropController {
drop_targets: Vec::new(),
}
}
pub(crate) fn new(hwnd: HWND, handler: Box<dyn Fn(FileDropEvent) -> bool>) -> Self {
let mut controller = FileDropController::default();
pub(crate) fn listen(
&mut self,
hwnd: HWND,
window: Rc<Window>,
handler: Box<dyn Fn(&Window, FileDropEvent) -> bool>,
) {
let listener = Rc::new(handler);
let handler = Rc::new(handler);
let mut callback = |hwnd| controller.inject_in_hwnd(hwnd, handler.clone());
// Enumerate child windows to find the WebView2 "window" and override!
enumerate_child_windows(hwnd, |hwnd| {
self.inject(hwnd, window.clone(), listener.clone())
});
{
let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback;
let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) };
let lparam = LPARAM(closure_pointer_pointer as _);
unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
let closure = &mut *(lparam.0 as *mut c_void as *mut &mut dyn FnMut(HWND) -> bool);
closure(hwnd).into()
}
unsafe { EnumChildWindows(hwnd, Some(enumerate_callback), lparam) };
}
controller
}
fn inject(
&mut self,
hwnd: HWND,
window: Rc<Window>,
listener: Rc<dyn Fn(&Window, FileDropEvent) -> bool>,
) -> bool {
// Safety: WinAPI calls are unsafe
unsafe {
let file_drop_handler: IDropTarget = FileDropHandler::new(window, listener).into();
if RevokeDragDrop(hwnd) != Err(DRAGDROP_E_INVALIDHWND.into())
&& RegisterDragDrop(hwnd, &file_drop_handler).is_ok()
{
// Not a great solution. But there is no reliable way to get the window handle of the webview, for whatever reason...
self.drop_targets.push(file_drop_handler);
}
fn inject_in_hwnd(&mut self, hwnd: HWND, handler: Rc<dyn Fn(FileDropEvent) -> bool>) -> bool {
let file_drop_handler: IDropTarget = FileDropHandler::new(hwnd, handler).into();
if unsafe { RevokeDragDrop(hwnd) } != Err(DRAGDROP_E_INVALIDHWND.into())
&& unsafe { RegisterDragDrop(hwnd, &file_drop_handler) }.is_ok()
{
self.drop_targets.push(file_drop_handler);
}
true
}
}
// https://gist.github.com/application-developer-DA/5a460d9ca02948f1d2bfa53100c941da
// Safety: WinAPI calls are unsafe
fn enumerate_child_windows<F>(hwnd: HWND, mut callback: F)
where
F: FnMut(HWND) -> bool,
{
let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback;
let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) };
let lparam = LPARAM(closure_pointer_pointer as _);
unsafe { EnumChildWindows(hwnd, Some(enumerate_callback), lparam) };
}
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()
}
#[implement(IDropTarget)]
pub struct FileDropHandler {
window: Rc<Window>,
listener: Rc<dyn Fn(&Window, FileDropEvent) -> bool>,
hwnd: HWND,
listener: Rc<dyn Fn(FileDropEvent) -> bool>,
cursor_effect: UnsafeCell<DROPEFFECT>,
hovered_is_valid: UnsafeCell<bool>, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */
}
impl FileDropHandler {
pub fn new(
window: Rc<Window>,
listener: Rc<dyn Fn(&Window, FileDropEvent) -> bool>,
) -> FileDropHandler {
pub fn new(hwnd: HWND, listener: Rc<dyn Fn(FileDropEvent) -> bool>) -> FileDropHandler {
Self {
window,
hwnd,
listener,
cursor_effect: DROPEFFECT_NONE.into(),
hovered_is_valid: false.into(),
@@ -208,16 +172,13 @@ impl IDropTarget_Impl for FileDropHandler {
*self.cursor_effect.get() = cursor_effect;
let mut pt = POINT { x: pt.x, y: pt.y };
ScreenToClient(HWND(self.window.hwnd() as _), &mut pt);
ScreenToClient(self.hwnd, &mut pt);
}
(self.listener)(
&self.window,
FileDropEvent::Hovered {
paths,
position: PhysicalPosition::new(pt.x as _, pt.y as _),
},
);
(self.listener)(FileDropEvent::Hovered {
paths,
position: (pt.x as _, pt.y as _),
});
Ok(())
}
@@ -234,7 +195,7 @@ impl IDropTarget_Impl for FileDropHandler {
fn DragLeave(&self) -> windows::core::Result<()> {
if unsafe { *self.hovered_is_valid.get() } {
(self.listener)(&self.window, FileDropEvent::Cancelled);
(self.listener)(FileDropEvent::Cancelled);
}
Ok(())
}
@@ -254,16 +215,13 @@ impl IDropTarget_Impl for FileDropHandler {
}
let mut pt = POINT { x: pt.x, y: pt.y };
ScreenToClient(HWND(self.window.hwnd() as _), &mut pt);
ScreenToClient(self.hwnd, &mut pt);
}
(self.listener)(
&self.window,
FileDropEvent::Dropped {
paths,
position: PhysicalPosition::new(pt.x as _, pt.y as _),
},
);
(self.listener)(FileDropEvent::Dropped {
paths,
position: (pt.x as _, pt.y as _),
});
Ok(())
}

View File

@@ -3,57 +3,48 @@
// SPDX-License-Identifier: MIT
mod file_drop;
mod resize;
use crate::{
webview::{
proxy::ProxyConfig, MemoryUsageLevel, PageLoadEvent, RequestAsyncResponder, WebContext,
WebViewAttributes, RGBA,
},
Error, Result,
};
use file_drop::FileDropController;
use url::Url;
use std::{
borrow::Cow,
collections::HashSet,
fmt::Write,
iter::once,
os::windows::prelude::OsStrExt,
path::PathBuf,
rc::Rc,
sync::{mpsc, Arc},
borrow::Cow, collections::HashSet, fmt::Write, iter::once, os::windows::prelude::OsStrExt,
path::PathBuf, rc::Rc, sync::mpsc,
};
use once_cell::{sync::Lazy, unsync::OnceCell};
use http::{Request, Response as HttpResponse, StatusCode};
use once_cell::sync::Lazy;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use url::Url;
use webview2_com::{Microsoft::Web::WebView2::Win32::*, *};
use windows::{
core::{s, ComInterface, PCSTR, PCWSTR, PWSTR},
Win32::{
Foundation::*,
Globalization::{self, MAX_LOCALE_NAME},
Graphics::Gdi::{RedrawWindow, HRGN, RDW_INTERNALPAINT},
Graphics::Gdi::{RedrawWindow, HBRUSH, HRGN, RDW_INTERNALPAINT},
System::{
Com::IStream,
LibraryLoader::{GetProcAddress, LoadLibraryW},
Com::{CoInitializeEx, IStream, COINIT_APARTMENTTHREADED},
LibraryLoader::{GetModuleHandleW, GetProcAddress, LoadLibraryW},
SystemInformation::OSVERSIONINFOW,
WinRT::EventRegistrationToken,
},
UI::{
Shell::{DefSubclassProc, SHCreateMemStream, SetWindowSubclass},
WindowsAndMessaging::{self as win32wm, PostMessageW, RegisterWindowMessageA},
WindowsAndMessaging::{
self as win32wm, CreateWindowExW, DefWindowProcW, DestroyWindow, PostMessageW,
RegisterClassExW, RegisterWindowMessageA, SetWindowPos, ShowWindow, CS_HREDRAW, CS_VREDRAW,
CW_USEDEFAULT, HCURSOR, HICON, HMENU, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOMOVE,
SWP_NOSIZE, SWP_NOZORDER, SW_HIDE, SW_SHOW, WINDOW_EX_STYLE, WNDCLASSEXW, WS_CHILD,
WS_CLIPCHILDREN, WS_VISIBLE,
},
},
},
};
use webview2_com::{Microsoft::Web::WebView2::Win32::*, *};
use crate::application::{platform::windows::WindowExtWindows, window::Window};
use http::{Request, Response as HttpResponse, StatusCode};
use self::file_drop::FileDropController;
use super::Theme;
use crate::{
proxy::ProxyConfig, Error, MemoryUsageLevel, PageLoadEvent, RequestAsyncResponder, Result,
WebContext, WebViewAttributes, RGBA,
};
impl From<webview2_com::Error> for Error {
fn from(err: webview2_com::Error) -> Self {
@@ -62,39 +53,137 @@ impl From<webview2_com::Error> for Error {
}
pub(crate) struct InnerWebView {
hwnd: HWND,
is_child: bool,
pub controller: ICoreWebView2Controller,
webview: ICoreWebView2,
env: ICoreWebView2Environment,
// Store FileDropController in here to make sure it gets dropped when
// the webview gets dropped, otherwise we'll have a memory leak
#[allow(dead_code)]
file_drop_controller: Rc<OnceCell<FileDropController>>,
file_drop_controller: Option<FileDropController>,
}
impl Drop for InnerWebView {
fn drop(&mut self) {
let _ = unsafe { self.controller.Close() };
if self.is_child {
let _ = unsafe { DestroyWindow(self.hwnd) };
}
}
}
impl InnerWebView {
pub fn new(
window: Rc<Window>,
window: &impl HasRawWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
web_context: Option<&mut WebContext>,
) -> Result<Self> {
let window = match window.raw_window_handle() {
RawWindowHandle::Win32(window) => window.hwnd as _,
_ => return Err(Error::UnsupportedWindowHandle),
};
Self::new_hwnd(HWND(window), attributes, pl_attrs, web_context)
}
pub fn new_as_child(
parent: &impl HasRawWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
web_context: Option<&mut WebContext>,
) -> Result<Self> {
let parent = match parent.raw_window_handle() {
RawWindowHandle::Win32(parent) => parent.hwnd as _,
_ => return Err(Error::UnsupportedWindowHandle),
};
let class_name = encode_wide("WRY_WEBVIEW");
unsafe extern "system" fn default_window_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
DefWindowProcW(hwnd, msg, wparam, lparam)
}
let class = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(default_window_proc),
cbClsExtra: 0,
cbWndExtra: 0,
hInstance: unsafe { HINSTANCE(GetModuleHandleW(PCWSTR::null()).unwrap_or_default().0) },
hIcon: HICON::default(),
hCursor: HCURSOR::default(), // must be null in order for cursor state to work properly
hbrBackground: HBRUSH::default(),
lpszMenuName: PCWSTR::null(),
lpszClassName: PCWSTR::from_raw(class_name.as_ptr()),
hIconSm: HICON::default(),
};
unsafe { RegisterClassExW(&class) };
let mut flags = WS_CHILD | WS_CLIPCHILDREN;
if attributes.visible {
flags |= WS_VISIBLE;
}
let child = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
PCWSTR::from_raw(class_name.as_ptr()),
PCWSTR::null(),
flags,
attributes.position.map(|a| a.0).unwrap_or(CW_USEDEFAULT),
attributes.position.map(|a| a.1).unwrap_or(CW_USEDEFAULT),
attributes.size.map(|a| a.0 as i32).unwrap_or(CW_USEDEFAULT),
attributes.size.map(|a| a.1 as i32).unwrap_or(CW_USEDEFAULT),
HWND(parent),
HMENU::default(),
GetModuleHandleW(PCWSTR::null()).unwrap_or_default(),
None,
)
};
Self::new_as_child_hwnd(child, attributes, pl_attrs, web_context)
}
fn new_as_child_hwnd(
hwnd: HWND,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
web_context: Option<&mut WebContext>,
) -> Result<Self> {
Self::new_hwnd(hwnd, attributes, pl_attrs, web_context).map(|mut w| {
w.is_child = true;
w
})
}
fn new_hwnd(
hwnd: HWND,
mut attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
web_context: Option<&mut WebContext>,
) -> Result<Self> {
let hwnd = HWND(window.hwnd() as _);
let file_drop_controller: Rc<OnceCell<FileDropController>> = Rc::new(OnceCell::new());
let file_drop_handler = attributes.file_drop_handler.take();
let file_drop_window = window.clone();
let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
let file_drop_controller = attributes
.file_drop_handler
.take()
.map(|handler| FileDropController::new(hwnd, handler));
let env = Self::create_environment(&web_context, pl_attrs.clone(), &attributes)?;
let controller = Self::create_controller(hwnd, &env, attributes.incognito)?;
let webview = Self::init_webview(window, hwnd, attributes, &env, &controller, pl_attrs)?;
if let Some(file_drop_handler) = file_drop_handler {
let mut controller = FileDropController::new();
controller.listen(hwnd, file_drop_window, file_drop_handler);
let _ = file_drop_controller.set(controller);
}
let webview = Self::init_webview(hwnd, attributes, &env, &controller, pl_attrs)?;
Ok(Self {
hwnd,
controller,
is_child: false,
webview,
env,
file_drop_controller,
@@ -227,7 +316,6 @@ impl InnerWebView {
}
fn init_webview(
window: Rc<Window>,
hwnd: HWND,
mut attributes: WebViewAttributes,
env: &ICoreWebView2Environment,
@@ -312,7 +400,6 @@ impl InnerWebView {
// document title changed handler
if let Some(document_title_changed_handler) = attributes.document_title_changed_handler {
let window_ = window.clone();
unsafe {
webview
.add_DocumentTitleChanged(
@@ -321,7 +408,7 @@ impl InnerWebView {
if let Some(webview) = webview {
webview.DocumentTitle(&mut title)?;
let title = take_pwstr(title);
document_title_changed_handler(&window_, title);
document_title_changed_handler(title);
}
Ok(())
})),
@@ -332,7 +419,7 @@ impl InnerWebView {
}
if let Some(on_page_load_handler) = attributes.on_page_load_handler {
let on_page_load_handler = Arc::new(on_page_load_handler);
let on_page_load_handler = Rc::new(on_page_load_handler);
let on_page_load_handler_ = on_page_load_handler.clone();
unsafe {
@@ -368,22 +455,13 @@ impl InnerWebView {
Self::add_script_to_execute_on_document_created(
&webview,
String::from(
r#"Object.defineProperty(window, 'ipc', {
value: Object.freeze({postMessage:s=>window.chrome.webview.postMessage(s)})
});
window.addEventListener('mousedown', (e) => {
if (e.buttons === 1) window.chrome.webview.postMessage('__WEBVIEW_LEFT_MOUSE_DOWN__')
});
window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('__WEBVIEW_MOUSE_MOVE__'));"#,
r#"Object.defineProperty(window, 'ipc', { value: Object.freeze({ postMessage: s=> window.chrome.webview.postMessage(s) }) });"#,
),
)?;
for js in attributes.initialization_scripts {
Self::add_script_to_execute_on_document_created(&webview, js)?;
}
let window_ = window.clone();
// Message handler
let ipc_handler = attributes.ipc_handler.take();
unsafe {
@@ -393,49 +471,8 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_
let mut js = PWSTR::null();
args.TryGetWebMessageAsString(&mut js)?;
let js = take_pwstr(js);
if js == "__WEBVIEW_LEFT_MOUSE_DOWN__" || js == "__WEBVIEW_MOUSE_MOVE__" {
if !window_.is_decorated() && window_.is_resizable() && !window_.is_maximized() {
use crate::application::window::CursorIcon;
let mut point = POINT::default();
win32wm::GetCursorPos(&mut point)?;
let result = resize::hit_test(window_.hwnd(), point.x, point.y);
let cursor = match result.0 as u32 {
win32wm::HTLEFT => CursorIcon::WResize,
win32wm::HTTOP => CursorIcon::NResize,
win32wm::HTRIGHT => CursorIcon::EResize,
win32wm::HTBOTTOM => CursorIcon::SResize,
win32wm::HTTOPLEFT => CursorIcon::NwResize,
win32wm::HTTOPRIGHT => CursorIcon::NeResize,
win32wm::HTBOTTOMLEFT => CursorIcon::SwResize,
win32wm::HTBOTTOMRIGHT => CursorIcon::SeResize,
_ => CursorIcon::Arrow,
};
// don't use `CursorIcon::Arrow` variant or cursor manipulation using css will cause cursor flickering
if cursor != CursorIcon::Arrow {
window_.set_cursor_icon(cursor);
}
if js == "__WEBVIEW_LEFT_MOUSE_DOWN__" {
// we ignore `HTCLIENT` variant so the webview receives the click correctly if it is not on the edges
// and prevent conflict with `tao::window::drag_window`.
if result.0 as u32 != win32wm::HTCLIENT {
resize::begin_resize_drag(
window_.hwnd(),
result.0,
win32wm::WM_NCLBUTTONDOWN,
point.x,
point.y,
)?;
}
}
}
// these are internal messages, ipc_handlers don't need it so exit early
return Ok(());
}
if let Some(ipc_handler) = &ipc_handler {
ipc_handler(&window_, js);
ipc_handler(js);
}
}
@@ -589,7 +626,6 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_
let env = env.clone();
let main_thread_id = std::thread::current().id();
let hwnd = window.hwnd();
unsafe {
webview
.add_WebResourceRequested(
@@ -852,7 +888,7 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_
unsafe {
controller
.SetIsVisible(true)
.SetIsVisible(attributes.visible)
.map_err(webview2_com::Error::WindowsError)?;
if attributes.focused {
controller
@@ -969,6 +1005,62 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_
set_theme(&self.webview, theme);
}
pub fn set_position(&self, position: (i32, i32)) {
if self.is_child {
unsafe {
let _ = SetWindowPos(
self.hwnd,
HWND::default(),
position.0,
position.1,
0,
0,
SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER,
);
}
}
}
pub fn set_size(&self, size: (u32, u32)) {
if self.is_child {
unsafe {
let _ = SetWindowPos(
self.hwnd,
HWND::default(),
0,
0,
size.0 as _,
size.1 as _,
SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER,
);
}
}
}
pub fn set_visible(&self, visible: bool) {
unsafe {
if self.is_child {
ShowWindow(
self.hwnd,
match visible {
true => SW_SHOW,
false => SW_HIDE,
},
);
}
let _ = self.controller.SetIsVisible(visible);
}
}
pub fn focus(&self) {
unsafe {
let _ = self
.controller
.MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
}
}
pub fn set_memory_usage_level(&self, level: MemoryUsageLevel) {
let Ok(webview) = self.webview.cast::<ICoreWebView2_19>() else {
return;
@@ -1125,11 +1217,8 @@ fn get_function_impl(library: &str, function: &str) -> Option<FARPROC> {
macro_rules! get_function {
($lib:expr, $func:ident) => {
crate::webview::webview2::get_function_impl(
concat!($lib, '\0'),
concat!(stringify!($func), '\0'),
)
.map(|f| unsafe { std::mem::transmute::<windows::Win32::Foundation::FARPROC, $func>(f) })
crate::webview2::get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0'))
.map(|f| unsafe { std::mem::transmute::<windows::Win32::Foundation::FARPROC, $func>(f) })
};
}
@@ -1171,7 +1260,7 @@ fn url_from_webview(webview: &ICoreWebView2) -> String {
static EXEC_MSG_ID: Lazy<u32> = Lazy::new(|| unsafe { RegisterWindowMessageA(s!("Wry::ExecMsg")) });
unsafe fn dispatch_handler<F>(hwnd: isize, function: F)
unsafe fn dispatch_handler<F>(hwnd: HWND, function: F)
where
F: FnMut() + 'static,
{
@@ -1181,7 +1270,7 @@ where
let raw = Box::into_raw(boxed2);
let res = PostMessageW(HWND(hwnd), *EXEC_MSG_ID, WPARAM(raw as _), LPARAM(0));
let res = PostMessageW(hwnd, *EXEC_MSG_ID, WPARAM(raw as _), LPARAM(0));
assert!(
res.is_ok(),
"PostMessage failed ; is the messages queue full?"

View File

@@ -5,7 +5,6 @@
use std::{
ffi::{c_void, CStr},
path::PathBuf,
rc::Rc,
};
use cocoa::{
@@ -18,10 +17,7 @@ use objc::{
};
use once_cell::sync::Lazy;
use crate::{
application::{dpi::LogicalPosition, platform::macos::WindowExtMacOS, window::Window},
webview::FileDropEvent,
};
use crate::FileDropEvent;
pub(crate) type NSDragOperation = cocoa::foundation::NSUInteger;
#[allow(non_upper_case_globals)]
@@ -61,20 +57,17 @@ static OBJC_DRAGGING_UPDATED: Lazy<extern "C" fn(*const Object, Sel, id) -> NSDr
// Safety: objc runtime calls are unsafe
pub(crate) unsafe fn set_file_drop_handler(
webview: *mut Object,
window: Rc<Window>,
handler: Box<dyn Fn(&Window, FileDropEvent) -> bool>,
) -> *mut (Box<dyn Fn(&Window, FileDropEvent) -> bool>, Rc<Window>) {
let listener = Box::into_raw(Box::new((handler, window)));
handler: Box<dyn Fn(FileDropEvent) -> bool>,
) -> *mut Box<dyn Fn(FileDropEvent) -> bool> {
let listener = Box::into_raw(Box::new(handler));
(*webview).set_ivar("FileDropHandler", listener as *mut _ as *mut c_void);
listener
}
#[allow(clippy::mut_from_ref)]
unsafe fn get_handler(
this: &Object,
) -> &mut (Box<dyn Fn(&Window, FileDropEvent) -> bool>, Rc<Window>) {
unsafe fn get_handler(this: &Object) -> &mut Box<dyn Fn(FileDropEvent) -> bool> {
let delegate: *mut c_void = *this.get_ivar("FileDropHandler");
&mut *(delegate as *mut (Box<dyn Fn(&Window, FileDropEvent) -> bool>, Rc<Window>))
&mut *(delegate as *mut Box<dyn Fn(FileDropEvent) -> bool>)
}
unsafe fn collect_paths(drag_info: id) -> Vec<PathBuf> {
@@ -116,13 +109,10 @@ extern "C" fn dragging_entered(this: &mut Object, sel: Sel, drag_info: id) -> NS
let paths = unsafe { collect_paths(drag_info) };
let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] };
let scale_factor = listener.1.scale_factor();
let ns_window = listener.1.ns_window() as id;
let frame: NSRect = unsafe { msg_send![ns_window, frame] };
let position =
LogicalPosition::<f64>::from((dl.x, frame.size.height - dl.y)).to_physical(scale_factor);
let frame: NSRect = unsafe { msg_send![this, frame] };
let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
if !listener.0(&listener.1, FileDropEvent::Hovered { paths, position }) {
if !listener(FileDropEvent::Hovered { paths, position }) {
// Reject the Wry file drop (invoke the OS default behaviour)
OBJC_DRAGGING_ENTERED(this, sel, drag_info)
} else {
@@ -135,13 +125,10 @@ extern "C" fn perform_drag_operation(this: &mut Object, sel: Sel, drag_info: id)
let paths = unsafe { collect_paths(drag_info) };
let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] };
let scale_factor = listener.1.scale_factor();
let ns_window = listener.1.ns_window() as id;
let frame: NSRect = unsafe { msg_send![ns_window, frame] };
let position =
LogicalPosition::<f64>::from((dl.x, frame.size.height - dl.y)).to_physical(scale_factor);
let frame: NSRect = unsafe { msg_send![this, frame] };
let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
if !listener.0(&listener.1, FileDropEvent::Dropped { paths, position }) {
if !listener(FileDropEvent::Dropped { paths, position }) {
// Reject the Wry file drop (invoke the OS default behaviour)
OBJC_PERFORM_DRAG_OPERATION(this, sel, drag_info)
} else {
@@ -151,7 +138,7 @@ extern "C" fn perform_drag_operation(this: &mut Object, sel: Sel, drag_info: id)
extern "C" fn dragging_exited(this: &mut Object, sel: Sel, drag_info: id) {
let listener = unsafe { get_handler(this) };
if !listener.0(&listener.1, FileDropEvent::Cancelled) {
if !listener(FileDropEvent::Cancelled) {
// Reject the Wry file drop (invoke the OS default behaviour)
OBJC_DRAGGING_EXITED(this, sel, drag_info);
}

View File

@@ -11,42 +11,36 @@ mod proxy;
#[cfg(target_os = "macos")]
mod synthetic_mouse_events;
use url::Url;
#[cfg(target_os = "macos")]
use cocoa::appkit::{NSView, NSViewHeightSizable, NSViewWidthSizable};
use cocoa::appkit::{NSView, NSViewHeightSizable, NSViewMinYMargin, NSViewWidthSizable};
use cocoa::{
base::{id, nil, NO, YES},
foundation::{NSDictionary, NSFastEnumeration, NSInteger},
};
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use url::Url;
use std::{
borrow::Cow,
ffi::{c_void, CStr},
os::raw::c_char,
ptr::{null, null_mut},
rc::Rc,
slice, str,
sync::{Arc, Mutex},
};
use core_graphics::geometry::CGRect;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use objc::{
declare::ClassDecl,
runtime::{Class, Object, Sel, BOOL},
};
use objc_id::Id;
#[cfg(target_os = "macos")]
use crate::application::platform::macos::WindowExtMacOS;
#[cfg(target_os = "macos")]
use file_drop::{add_file_drop_methods, set_file_drop_handler};
#[cfg(target_os = "ios")]
use crate::application::platform::ios::WindowExtIOS;
#[cfg(feature = "mac-proxy")]
use crate::webview::{
use crate::{
proxy::ProxyConfig,
wkwebview::proxy::{
nw_endpoint_t, nw_proxy_config_create_http_connect, nw_proxy_config_create_socksv5,
@@ -54,21 +48,14 @@ use crate::webview::{
};
use crate::{
application::{
dpi::{LogicalSize, PhysicalSize},
window::Window,
},
webview::{
wkwebview::{
download::{
add_download_methods, download_did_fail, download_did_finish, download_policy,
set_download_delegate,
},
navigation::{add_navigation_mathods, drop_navigation_methods, set_navigation_methods},
wkwebview::{
download::{
add_download_methods, download_did_fail, download_did_finish, download_policy,
set_download_delegate,
},
FileDropEvent, PageLoadEvent, RequestAsyncResponder, WebContext, WebViewAttributes, RGBA,
navigation::{add_navigation_mathods, drop_navigation_methods, set_navigation_methods},
},
Result,
Error, PageLoadEvent, RequestAsyncResponder, Result, WebContext, WebViewAttributes, RGBA,
};
use http::{
@@ -79,6 +66,7 @@ use http::{
};
const IPC_MESSAGE_HANDLER_NAME: &str = "ipc";
#[cfg(target_os = "macos")]
const ACCEPT_FIRST_MOUSE: &str = "accept_first_mouse";
const NS_JSON_WRITING_FRAGMENTS_ALLOWED: u64 = 4;
@@ -88,25 +76,61 @@ pub(crate) struct InnerWebView {
#[cfg(target_os = "macos")]
pub ns_window: id,
pub manager: id,
is_child: bool,
pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
// Note that if following functions signatures are changed in the future,
// all functions pointer declarations in objc callbacks below all need to get updated.
ipc_handler_ptr: *mut (Box<dyn Fn(&Window, String)>, Rc<Window>),
document_title_changed_handler: *mut (Box<dyn Fn(&Window, String)>, Rc<Window>),
ipc_handler_ptr: *mut Box<dyn Fn(String)>,
document_title_changed_handler: *mut Box<dyn Fn(String)>,
navigation_decide_policy_ptr: *mut Box<dyn Fn(String, bool) -> bool>,
page_load_handler: *mut Box<dyn Fn(PageLoadEvent)>,
#[cfg(target_os = "macos")]
file_drop_ptr: *mut (Box<dyn Fn(&Window, FileDropEvent) -> bool>, Rc<Window>),
file_drop_ptr: *mut Box<dyn Fn(crate::FileDropEvent) -> bool>,
download_delegate: id,
protocol_ptrs: Vec<*mut Box<dyn Fn(Request<Vec<u8>>, RequestAsyncResponder)>>,
}
impl InnerWebView {
pub fn new(
window: Rc<Window>,
window: &impl HasRawWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
_web_context: Option<&mut WebContext>,
) -> Result<Self> {
let ns_view = match window.raw_window_handle() {
#[cfg(target_os = "macos")]
RawWindowHandle::AppKit(w) => w.ns_view,
#[cfg(target_os = "ios")]
RawWindowHandle::UiKit(w) => w.ui_view,
_ => return Err(Error::UnsupportedWindowHandle),
};
Self::new_ns_view(ns_view as _, attributes, pl_attrs, _web_context, false)
}
pub fn new_as_child(
window: &impl HasRawWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
_web_context: Option<&mut WebContext>,
) -> Result<Self> {
let ns_view = match window.raw_window_handle() {
#[cfg(target_os = "macos")]
RawWindowHandle::AppKit(w) => w.ns_view,
#[cfg(target_os = "ios")]
RawWindowHandle::UiKit(w) => w.ui_view,
_ => return Err(Error::UnsupportedWindowHandle),
};
Self::new_ns_view(ns_view as _, attributes, pl_attrs, _web_context, true)
}
fn new_ns_view(
ns_view: id,
attributes: WebViewAttributes,
_pl_attrs: super::PlatformSpecificWebViewAttributes,
_web_context: Option<&mut WebContext>,
is_child: bool,
) -> Result<Self> {
// Function for ipc handler
extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) {
@@ -114,13 +138,12 @@ impl InnerWebView {
unsafe {
let function = this.get_ivar::<*mut c_void>("function");
if !function.is_null() {
let function =
&mut *(*function as *mut (Box<dyn for<'r> Fn(&'r Window, String)>, Rc<Window>));
let function = &mut *(*function as *mut Box<dyn Fn(String)>);
let body: id = msg_send![msg, body];
let utf8: *const c_char = msg_send![body, UTF8String];
let js = CStr::from_ptr(utf8).to_str().expect("Invalid UTF8 string");
(function.0)(&function.1, js.to_string());
(function)(js.to_string());
} else {
log::warn!("WebView instance is dropped! This handler shouldn't be called.");
}
@@ -371,17 +394,37 @@ impl InnerWebView {
#[cfg(target_os = "macos")]
{
use core_graphics::geometry::{CGPoint, CGSize};
let frame: CGRect = CGRect::new(&CGPoint::new(0., 0.), &CGSize::new(0., 0.));
let (x, y) = attributes.position.unwrap_or((0, 0));
let (w, h) = attributes.size.unwrap_or_else(|| {
if is_child {
let frame = NSView::frame(ns_view);
(frame.size.width as u32, frame.size.height as u32)
} else {
(0, 0)
}
});
let frame: CGRect = CGRect::new(
&window_position(
if is_child { ns_view } else { webview },
(x, y),
(w as f64, h as f64),
),
&CGSize::new(w as f64, h as f64),
);
let _: () = msg_send![webview, initWithFrame:frame configuration:config];
// Auto-resize on macOS
webview.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable);
if is_child {
// fixed element
webview.setAutoresizingMask_(NSViewMinYMargin);
} else {
// Auto-resize
webview.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable);
}
}
#[cfg(target_os = "ios")]
{
let ui_view = window.ui_view() as id;
let frame: CGRect = msg_send![ui_view, frame];
let frame: CGRect = msg_send![ns_view, frame];
// set all autoresizingmasks
let () = msg_send![webview, setAutoresizingMask: 31];
let _: () = msg_send![webview, initWithFrame:frame configuration:config];
@@ -391,6 +434,10 @@ impl InnerWebView {
let _: () = msg_send![scroll, setBounces: NO];
}
if !attributes.visible {
let () = msg_send![webview, setHidden: YES];
}
#[cfg(any(debug_assertions, feature = "devtools"))]
if attributes.devtools {
let has_inspectable_property: BOOL =
@@ -425,7 +472,7 @@ impl InnerWebView {
None => class!(WebViewDelegate),
};
let handler: id = msg_send![cls, new];
let ipc_handler_ptr = Box::into_raw(Box::new((ipc_handler, window.clone())));
let ipc_handler_ptr = Box::into_raw(Box::new(ipc_handler));
(*handler).set_ivar("function", ipc_handler_ptr as *mut _ as *mut c_void);
let ipc = NSString::new(IPC_MESSAGE_HANDLER_NAME);
@@ -460,10 +507,9 @@ impl InnerWebView {
unsafe {
let function = this.get_ivar::<*mut c_void>("function");
if !function.is_null() {
let function = &mut *(*function
as *mut (Box<dyn for<'r> Fn(&'r Window, String)>, Rc<Window>));
let function = &mut *(*function as *mut Box<dyn Fn(String)>);
let title: id = msg_send![of_object, title];
(function.0)(&function.1, NSString(title).to_str().to_string());
(function)(NSString(title).to_str().to_string());
}
}
}
@@ -475,7 +521,7 @@ impl InnerWebView {
let handler: id = msg_send![cls, new];
let document_title_changed_handler =
Box::into_raw(Box::new((document_title_changed_handler, window.clone())));
Box::into_raw(Box::new(document_title_changed_handler));
(*handler).set_ivar(
"function",
@@ -752,17 +798,15 @@ impl InnerWebView {
#[cfg(target_os = "macos")]
let file_drop_ptr = match attributes.file_drop_handler {
// if we have a file_drop_handler defined, use the defined handler
Some(file_drop_handler) => {
set_file_drop_handler(webview, window.clone(), file_drop_handler)
}
Some(file_drop_handler) => set_file_drop_handler(webview, file_drop_handler),
// prevent panic by using a blank handler
None => set_file_drop_handler(webview, window.clone(), Box::new(|_, _| false)),
None => set_file_drop_handler(webview, Box::new(|_| false)),
};
// ns window is required for the print operation
#[cfg(target_os = "macos")]
let ns_window = {
let ns_window = window.ns_window() as id;
let ns_window: id = msg_send![ns_view, window];
let can_set_titlebar_style: BOOL = msg_send![
ns_window,
@@ -790,6 +834,7 @@ impl InnerWebView {
page_load_handler,
download_delegate,
protocol_ptrs,
is_child,
};
// Initialize scripts
@@ -825,37 +870,41 @@ r#"Object.defineProperty(window, 'ipc', {
// Inject the web view into the window as main content
#[cfg(target_os = "macos")]
{
let parent_view_cls = match ClassDecl::new("WryWebViewParent", class!(NSView)) {
Some(mut decl) => {
decl.add_method(
sel!(keyDown:),
key_down as extern "C" fn(&mut Object, Sel, id),
);
if is_child {
let _: () = msg_send![ns_view, addSubview: webview];
} else {
let parent_view_cls = match ClassDecl::new("WryWebViewParent", class!(NSView)) {
Some(mut decl) => {
decl.add_method(
sel!(keyDown:),
key_down as extern "C" fn(&mut Object, Sel, id),
);
extern "C" fn key_down(_this: &mut Object, _sel: Sel, event: id) {
unsafe {
let app = cocoa::appkit::NSApp();
let menu: id = msg_send![app, mainMenu];
let () = msg_send![menu, performKeyEquivalent: event];
extern "C" fn key_down(_this: &mut Object, _sel: Sel, event: id) {
unsafe {
let app = cocoa::appkit::NSApp();
let menu: id = msg_send![app, mainMenu];
let () = msg_send![menu, performKeyEquivalent: event];
}
}
decl.register()
}
None => class!(NSView),
};
decl.register()
}
None => class!(NSView),
};
let parent_view: id = msg_send![parent_view_cls, alloc];
let _: () = msg_send![parent_view, init];
parent_view.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable);
let _: () = msg_send![parent_view, addSubview: webview];
let parent_view: id = msg_send![parent_view_cls, alloc];
let _: () = msg_send![parent_view, init];
parent_view.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable);
let _: () = msg_send![parent_view, addSubview: webview];
// inject the webview into the window
let ns_window = window.ns_window() as id;
// Tell the webview receive keyboard events in the window.
// See https://github.com/tauri-apps/wry/issues/739
let _: () = msg_send![ns_window, setContentView: parent_view];
let _: () = msg_send![ns_window, makeFirstResponder: webview];
// inject the webview into the window
let ns_window: id = msg_send![ns_view, window];
// Tell the webview receive keyboard events in the window.
// See https://github.com/tauri-apps/wry/issues/739
let _: () = msg_send![ns_window, setContentView: parent_view];
let _: () = msg_send![ns_window, makeFirstResponder: webview];
}
// make sure the window is always on top when we create a new webview
let app_class = class!(NSApplication);
@@ -865,8 +914,7 @@ r#"Object.defineProperty(window, 'ipc', {
#[cfg(target_os = "ios")]
{
let ui_view = window.ui_view() as id;
let _: () = msg_send![ui_view, addSubview: webview];
let _: () = msg_send![ns_view, addSubview: webview];
}
Ok(w)
@@ -1030,13 +1078,6 @@ r#"Object.defineProperty(window, 'ipc', {
false
}
#[cfg(target_os = "macos")]
pub fn inner_size(&self, scale_factor: f64) -> PhysicalSize<u32> {
let view_frame = unsafe { NSView::frame(self.webview) };
let logical: LogicalSize<f64> = (view_frame.size.width, view_frame.size.height).into();
logical.to_physical(scale_factor)
}
pub fn zoom(&self, scale_factor: f64) {
unsafe {
let _: () = msg_send![self.webview, setPageZoom: scale_factor];
@@ -1046,6 +1087,43 @@ r#"Object.defineProperty(window, 'ipc', {
pub fn set_background_color(&self, _background_color: RGBA) -> Result<()> {
Ok(())
}
pub fn set_position(&self, position: (i32, i32)) {
if self.is_child {
unsafe {
let mut frame: CGRect = msg_send![self.webview, frame];
frame.origin = window_position(
msg_send![self.webview, superview],
(position.0, position.1),
(frame.size.width, frame.size.height),
);
let () = msg_send![self.webview, setFrame: frame];
}
}
}
pub fn set_size(&self, size: (u32, u32)) {
if self.is_child {
unsafe {
let mut frame: CGRect = msg_send![self.webview, frame];
frame.size = CGSize::new(size.0 as f64, size.1 as f64);
let () = msg_send![self.webview, setFrame: frame];
}
}
}
pub fn set_visible(&self, visible: bool) {
unsafe {
let () = msg_send![self.webview, setHidden: !visible];
}
}
pub fn focus(&self) {
unsafe {
let window: id = msg_send![self.webview, window];
let _: () = msg_send![window, makeFirstResponder: self.webview];
}
}
}
pub fn url_from_webview(webview: id) -> String {
@@ -1179,3 +1257,14 @@ impl From<NSData> for NSString {
}
struct NSData(id);
/// Converts from wry screen-coordinates to macOS screen-coordinates.
/// wry: top-left is (0, 0) and y increasing downwards
/// macOS: bottom-left is (0, 0) and y increasing upwards
unsafe fn window_position(view: id, position: (i32, i32), size: (f64, f64)) -> CGPoint {
let frame: CGRect = msg_send![view, frame];
CGPoint::new(
position.0 as f64,
frame.size.height - position.1 as f64 - size.1,
)
}

View File

@@ -11,7 +11,7 @@ use objc::{
};
use super::{url_from_webview, InnerWebView, NSString};
use crate::webview::PageLoadEvent;
use crate::PageLoadEvent;
extern "C" fn did_commit_navigation(this: &Object, _: Sel, webview: id, _navigation: id) {
unsafe {

View File

@@ -5,7 +5,7 @@
use cocoa::base::nil;
use libc::c_char;
use crate::{webview::proxy::ProxyEndpoint, Error};
use crate::{proxy::ProxyEndpoint, Error};
use super::NSString;