mod web_context;
pub use web_context::WebContext;
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
mod webkitgtk;
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
use webkitgtk::*;
#[cfg(any(target_os = "macos", target_os = "ios"))]
mod wkwebview;
#[cfg(any(target_os = "macos", target_os = "ios"))]
use wkwebview::*;
#[cfg(target_os = "windows")]
mod webview2;
#[cfg(target_os = "windows")]
use self::webview2::*;
use crate::{Error, Result};
use std::{path::PathBuf, rc::Rc};
use serde_json::Value;
use url::Url;
#[cfg(target_os = "windows")]
use crate::application::platform::windows::WindowExtWindows;
use crate::application::window::Window;
pub struct WebViewAttributes {
pub visible: bool,
pub transparent: bool,
pub url: Option<Url>,
pub html: Option<String>,
pub initialization_scripts: Vec<String>,
pub custom_protocols: Vec<(String, Box<dyn Fn(&str) -> Result<(Vec<u8>, String)>>)>,
pub rpc_handler: Option<Box<dyn Fn(&Window, RpcRequest) -> Option<RpcResponse>>>,
#[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>>,
}
impl Default for WebViewAttributes {
fn default() -> Self {
Self {
visible: true,
transparent: false,
url: None,
html: None,
initialization_scripts: vec![],
custom_protocols: vec![],
rpc_handler: None,
file_drop_handler: None,
}
}
}
pub struct WebViewBuilder<'a> {
pub webview: WebViewAttributes,
web_context: Option<&'a mut WebContext>,
window: Window,
}
impl<'a> WebViewBuilder<'a> {
pub fn new(window: Window) -> Result<Self> {
let webview = WebViewAttributes::default();
let web_context = None;
Ok(Self {
webview,
web_context,
window,
})
}
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.webview.transparent = transparent;
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.webview.visible = visible;
self
}
pub fn with_initialization_script(mut self, js: &str) -> Self {
self.webview.initialization_scripts.push(js.to_string());
self
}
#[cfg(feature = "protocol")]
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
where
F: Fn(&str) -> Result<(Vec<u8>, String)> + 'static,
{
self
.webview
.custom_protocols
.push((name, Box::new(handler)));
self
}
pub fn with_rpc_handler<F>(mut self, handler: F) -> Self
where
F: Fn(&Window, RpcRequest) -> Option<RpcResponse> + 'static,
{
self.webview.rpc_handler = Some(Box::new(handler));
self
}
#[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
}
pub fn with_url(mut self, url: &str) -> Result<Self> {
self.webview.url = Some(Url::parse(url)?);
Ok(self)
}
pub fn with_html(mut self, html: impl Into<String>) -> Result<Self> {
self.webview.html = Some(html.into());
Ok(self)
}
pub fn with_web_context(mut self, web_context: &'a mut WebContext) -> Self {
self.web_context = Some(web_context);
self
}
pub fn build(mut self) -> Result<WebView> {
if self.webview.rpc_handler.is_some() {
let js = r#"
(function() {
function Rpc() {
const self = this;
this._promises = {};
// Private internal function called on error
this._error = (id, error) => {
if(this._promises[id]){
this._promises[id].reject(error);
delete this._promises[id];
}
}
// Private internal function called on result
this._result = (id, result) => {
if(this._promises[id]){
this._promises[id].resolve(result);
delete this._promises[id];
}
}
// Call remote method and expect a reply from the handler
this.call = function(method) {
let array = new Uint32Array(1);
window.crypto.getRandomValues(array);
const id = array[0];
const params = Array.prototype.slice.call(arguments, 1);
const payload = {jsonrpc: "2.0", id, method, params};
const promise = new Promise((resolve, reject) => {
self._promises[id] = {resolve, reject};
});
window.external.invoke(JSON.stringify(payload));
return promise;
}
// Send a notification without an `id` so no reply is expected.
this.notify = function(method) {
const params = Array.prototype.slice.call(arguments, 1);
const payload = {jsonrpc: "2.0", method, params};
window.external.invoke(JSON.stringify(payload));
return Promise.resolve();
}
}
window.external = window.external || {};
window.external.rpc = new Rpc();
window.rpc = window.external.rpc;
})();
"#;
self.webview.initialization_scripts.push(js.to_string());
}
let window = Rc::new(self.window);
let webview = InnerWebView::new(window.clone(), self.webview, self.web_context)?;
Ok(WebView { window, webview })
}
}
pub struct WebView {
window: Rc<Window>,
webview: InnerWebView,
}
impl Drop for WebView {
fn drop(&mut self) {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
unsafe {
use crate::application::platform::unix::WindowExtUnix;
use gtk::prelude::WidgetExtManual;
self.window().gtk_window().destroy();
}
#[cfg(target_os = "windows")]
unsafe {
use winapi::{shared::windef::HWND, um::winuser::DestroyWindow};
DestroyWindow(self.window.hwnd() as HWND);
}
}
}
impl WebView {
pub fn new(window: Window) -> Result<Self> {
WebViewBuilder::new(window)?.build()
}
pub fn window(&self) -> &Window {
&self.window
}
pub fn evaluate_script(&self, js: &str) -> Result<()> {
self.webview.eval(js)
}
pub fn print(&self) -> Result<()> {
self.webview.print();
Ok(())
}
pub fn resize(&self) -> Result<()> {
#[cfg(target_os = "windows")]
self.webview.resize(self.window.hwnd())?;
Ok(())
}
pub fn focus(&self) {
self.webview.focus();
}
}
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);
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 {
RpcResponse::get_result_script(id, Value::Null)?
};
Ok(Some(js))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
const RPC_VERSION: &str = "2.0";
#[derive(Debug, Serialize, Deserialize)]
pub struct RpcRequest {
jsonrpc: String,
pub id: Option<Value>,
pub method: String,
pub params: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RpcResponse {
jsonrpc: String,
pub(crate) id: Option<Value>,
pub(crate) result: Option<Value>,
pub(crate) error: Option<Value>,
}
impl RpcResponse {
pub fn new_result(id: Option<Value>, result: Option<Value>) -> Self {
Self {
jsonrpc: RPC_VERSION.to_string(),
id,
result,
error: None,
}
}
pub fn new_error(id: Option<Value>, error: Option<Value>) -> Self {
Self {
jsonrpc: RPC_VERSION.to_string(),
id,
error,
result: None,
}
}
pub fn get_result_script(id: Value, result: Value) -> Result<String> {
let retval = serde_json::to_string(&result)?;
Ok(format!(
"window.external.rpc._result({}, {})",
id.to_string(),
retval
))
}
pub fn get_error_script(id: Value, result: Value) -> Result<String> {
let retval = serde_json::to_string(&result)?;
Ok(format!(
"window.external.rpc._error({}, {})",
id.to_string(),
retval
))
}
}
#[non_exhaustive]
#[derive(Debug, Serialize, Clone)]
pub enum FileDropEvent {
Hovered(Vec<PathBuf>),
Dropped(Vec<PathBuf>),
Cancelled,
}
pub fn webview_version() -> Result<String> {
platform_webview_version()
}
#[cfg(target_os = "windows")]
pub trait WebviewExtWindows {
fn controller(&self) -> Option<&::webview2::Controller>;
}
#[cfg(target_os = "windows")]
impl WebviewExtWindows for WebView {
fn controller(&self) -> Option<&::webview2::Controller> {
self.webview.controller.get()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_get_webview_version() {
if let Err(error) = webview_version() {
panic!("{}", error);
}
}
}