Skip to content

Commit

Permalink
Add WebViewAttributes (#286)
Browse files Browse the repository at this point in the history
* Add WebViewAttributes

* Update InnerWebView constructor on Windows

* Update InnerWebView constructor on macOS

* Update InnerWebView constructor on Linux

* cargo fmt

* Add change file

* cargo clippy
  • Loading branch information
Ngo Iok Ui (Wu Yu Wei) authored Jun 4, 2021
1 parent b949e57 commit 81f3218
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 170 deletions.
5 changes: 5 additions & 0 deletions .changes/attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wry": patch
---

Add WebViewAttributes
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@

#![allow(clippy::new_without_default)]
#![allow(clippy::wrong_self_convention)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
#![allow(clippy::unit_cmp)]
#![allow(clippy::upper_case_acronyms)]
Expand Down
241 changes: 143 additions & 98 deletions src/webview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,34 +37,58 @@ use crate::application::window::Window;
#[cfg(feature = "winrt")]
use windows_webview2::Windows::Win32::WindowsAndMessaging::HWND;

// Helper so all platforms handle RPC messages consistently.
fn rpc_proxy(
window: &Window,
js: String,
handler: &dyn Fn(&Window, RpcRequest) -> Option<RpcResponse>,
) -> Result<Option<String>> {
let req = serde_json::from_str::<RpcRequest>(&js)
.map_err(|e| Error::RpcScriptError(e.to_string(), js))?;
pub struct WebViewAttributes {
/// Whether the WebView window should be visible.
pub visible: bool,
/// Whether the WebView should be transparent.
pub transparent: bool,
/// Whether load the provided URL to [`WebView`].
pub url: Option<Url>,
/// Initialize javascript code when loading new pages. When webview load a new page, this
/// initialization code will be executed. It is guaranteed that code is executed before
/// `window.onload`.
pub initialization_scripts: Vec<String>,
/// Register custom file loading protocol
pub custom_protocols: Vec<(String, Box<dyn Fn(&Window, &str) -> Result<Vec<u8>>>)>,
/// Set the RPC handler to Communicate between the host Rust code and Javascript on webview.
///
/// The communication is done via [JSON-RPC](https://www.jsonrpc.org). Users can use this to register an incoming
/// request handler and reply with responses that are passed back to Javascript. On the Javascript
/// side the client is exposed via `window.rpc` with two public methods:
///
/// 1. The `call()` function accepts a method name and parameters and expects a reply.
/// 2. The `notify()` function accepts a method name and parameters but does not expect a reply.
///
/// Both functions return promises but `notify()` resolves immediately.
pub rpc_handler: Option<Box<dyn Fn(&Window, RpcRequest) -> Option<RpcResponse>>>,
/// Set a handler closure to process incoming [`FileDropEvent`] of the webview.
///
/// # Blocking OS Default Behavior
/// Return `true` in the callback to block the OS' default behavior of handling a file drop.
///
/// Note, that if you do block this behavior, it won't be possible to drop files on `<input type="file">` forms.
/// Also note, that it's not possible to manually set the value of a `<input type="file">` via JavaScript for security reasons.
#[cfg(feature = "file-drop")]
pub file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
#[cfg(not(feature = "file-drop"))]
file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
/// Whether the WebView window should have a custom user data path. This is useful in Windows
/// when a bundled application can't have the webview data inside `Program Files`.
pub data_directory: Option<PathBuf>,
}

let mut response = (handler)(window, req);
// Got a synchronous response so convert it to a script to be evaluated
if let Some(mut response) = response.take() {
if let Some(id) = response.id {
let js = if let Some(error) = response.error.take() {
RpcResponse::get_error_script(id, error)?
} else if let Some(result) = response.result.take() {
RpcResponse::get_result_script(id, result)?
} else {
// No error or result, assume a positive response
// with empty result (ACK)
RpcResponse::get_result_script(id, Value::Null)?
};
Ok(Some(js))
} else {
Ok(None)
impl Default for WebViewAttributes {
fn default() -> Self {
Self {
visible: true,
transparent: false,
url: None,
initialization_scripts: vec![],
custom_protocols: vec![],
rpc_handler: None,
file_drop_handler: None,
data_directory: None,
}
} else {
Ok(None)
}
}

Expand All @@ -74,56 +98,50 @@ fn rpc_proxy(
/// scripts for those who prefer to control fine grained window creation and event handling.
/// [`WebViewBuilder`] privides ability to setup initialization before web engine starts.
pub struct WebViewBuilder {
transparent: bool,
pub webview: WebViewAttributes,
window: Window,
tx: Sender<String>,
rx: Receiver<String>,
initialization_scripts: Vec<String>,
window: Window,
url: Option<Url>,
custom_protocols: Vec<(String, Box<dyn Fn(&Window, &str) -> Result<Vec<u8>>>)>,
rpc_handler: Option<Box<dyn Fn(&Window, RpcRequest) -> Option<RpcResponse>>>,
file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
data_directory: Option<PathBuf>,
}

impl WebViewBuilder {
/// Create [`WebViewBuilder`] from provided [`Window`].
pub fn new(window: Window) -> Result<Self> {
let (tx, rx) = channel();
let webview = WebViewAttributes::default();

Ok(Self {
webview,
window,
tx,
rx,
initialization_scripts: vec![],
window,
url: None,
transparent: false,
custom_protocols: vec![],
rpc_handler: None,
file_drop_handler: None,
data_directory: None,
})
}

/// Whether the WebView window should be transparent. If this is true, writing colors
/// with alpha values different than `1.0` will produce a transparent window.
/// Sets whether the WebView should be transparent.
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.transparent = transparent;
self.webview.transparent = transparent;
self
}

/// Initialize javascript code when loading new pages. Everytime webview load a new page, this
/// Sets whether the WebView should be transparent.
pub fn with_visible(mut self, visible: bool) -> Self {
self.webview.visible = visible;
self
}

/// Initialize javascript code when loading new pages. When webview load a new page, this
/// initialization code will be executed. It is guaranteed that code is executed before
/// `window.onload`.
pub fn with_initialization_script(mut self, js: &str) -> Self {
self.initialization_scripts.push(js.to_string());
self.webview.initialization_scripts.push(js.to_string());
self
}

/// Whether the WebView window should have a custom user data path. This is usefull in Windows
/// Whether the WebView window should have a custom user data path. This is useful in Windows
/// when a bundled application can't have the webview data inside `Program Files`.
pub fn with_data_directory(mut self, data_directory: PathBuf) -> Self {
self.data_directory.replace(data_directory);
self.webview.data_directory.replace(data_directory);
self
}

Expand All @@ -140,7 +158,10 @@ impl WebViewBuilder {
where
F: Fn(&Window, &str) -> Result<Vec<u8>> + 'static,
{
self.custom_protocols.push((name, Box::new(handler)));
self
.webview
.custom_protocols
.push((name, Box::new(handler)));
self
}

Expand All @@ -158,7 +179,44 @@ impl WebViewBuilder {
where
F: Fn(&Window, RpcRequest) -> Option<RpcResponse> + 'static,
{
let js = r#"
self.webview.rpc_handler = Some(Box::new(handler));
self
}

/// Set a handler closure to process incoming [`FileDropEvent`] of the webview.
///
/// # Blocking OS Default Behavior
/// Return `true` in the callback to block the OS' default behavior of handling a file drop.
///
/// Note, that if you do block this behavior, it won't be possible to drop files on `<input type="file">` forms.
/// Also note, that it's not possible to manually set the value of a `<input type="file">` via JavaScript for security reasons.
#[cfg(feature = "file-drop")]
pub fn with_file_drop_handler<F>(mut self, handler: F) -> Self
where
F: Fn(&Window, FileDropEvent) -> bool + 'static,
{
self.webview.file_drop_handler = Some(Box::new(handler));
self
}

/// Load the provided URL when the builder calling [`WebViewBuilder::build`] to create the
/// [`WebView`]. The provided URL must be valid.
pub fn with_url(mut self, url: &str) -> Result<Self> {
self.webview.url = Some(Url::parse(url)?);
Ok(self)
}

/// Consume the builder and create the [`WebView`].
///
/// Platform-specific behavior:
///
/// - **Unix:** This method must be called in a gtk thread. Usually this means it should be
/// called in the same thread with the [`EventLoop`] you create.
///
/// [`EventLoop`]: crate::application::event_loop::EventLoop
pub fn build(mut self) -> Result<WebView> {
if self.webview.rpc_handler.is_some() {
let js = r#"
(function() {
function Rpc() {
const self = this;
Expand Down Expand Up @@ -208,54 +266,10 @@ impl WebViewBuilder {
})();
"#;

self.initialization_scripts.push(js.to_string());
self.rpc_handler = Some(Box::new(handler));
self
}

/// Set a handler closure to process incoming [`FileDropEvent`] of the webview.
///
/// # Blocking OS Default Behavior
/// Return `true` in the callback to block the OS' default behavior of handling a file drop.
///
/// Note, that if you do block this behavior, it won't be possible to drop files on `<input type="file">` forms.
/// Also note, that it's not possible to manually set the value of a `<input type="file">` via JavaScript for security reasons.
#[cfg(feature = "file-drop")]
pub fn with_file_drop_handler<F>(mut self, handler: F) -> Self
where
F: Fn(&Window, FileDropEvent) -> bool + 'static,
{
self.file_drop_handler = Some(Box::new(handler));
self
}

/// Load the provided URL when the builder calling [`WebViewBuilder::build`] to create the
/// [`WebView`]. The provided URL must be valid.
pub fn with_url(mut self, url: &str) -> Result<Self> {
self.url = Some(Url::parse(url)?);
Ok(self)
}

/// Consume the builder and create the [`WebView`].
///
/// Platform-specific behavior:
///
/// - **Unix:** This method must be called in a gtk thread. Usually this means it should be
/// called in the same thread with the [`EventLoop`] you create.
///
/// [`EventLoop`]: crate::application::event_loop::EventLoop
pub fn build(self) -> Result<WebView> {
self.webview.initialization_scripts.push(js.to_string());
}
let window = Rc::new(self.window);
let webview = InnerWebView::new(
window.clone(),
self.initialization_scripts,
self.url,
self.transparent,
self.custom_protocols,
self.rpc_handler,
self.file_drop_handler,
self.data_directory,
)?;
let webview = InnerWebView::new(window.clone(), self.webview)?;
Ok(WebView {
window,
webview,
Expand Down Expand Up @@ -375,6 +389,37 @@ impl Dispatcher {
}
}

// Helper so all platforms handle RPC messages consistently.
fn rpc_proxy(
window: &Window,
js: String,
handler: &dyn Fn(&Window, RpcRequest) -> Option<RpcResponse>,
) -> Result<Option<String>> {
let req = serde_json::from_str::<RpcRequest>(&js)
.map_err(|e| Error::RpcScriptError(e.to_string(), js))?;

let mut response = (handler)(window, req);
// Got a synchronous response so convert it to a script to be evaluated
if let Some(mut response) = response.take() {
if let Some(id) = response.id {
let js = if let Some(error) = response.error.take() {
RpcResponse::get_error_script(id, error)?
} else if let Some(result) = response.result.take() {
RpcResponse::get_result_script(id, result)?
} else {
// No error or result, assume a positive response
// with empty result (ACK)
RpcResponse::get_result_script(id, Value::Null)?
};
Ok(Some(js))
} else {
Ok(None)
}
} else {
Ok(None)
}
}

const RPC_VERSION: &str = "2.0";

/// RPC request message.
Expand Down
Loading

0 comments on commit 81f3218

Please sign in to comment.