mod proxy;
mod web_context;
pub use web_context::WebContext;
#[cfg(target_os = "android")]
pub(crate) mod android;
#[cfg(target_os = "android")]
pub mod prelude {
pub use super::android::{binding::*, dispatch, find_class, setup, Context};
}
#[cfg(target_os = "android")]
pub use android::JniHandle;
#[cfg(target_os = "android")]
use android::*;
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) 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"))]
pub(crate) mod wkwebview;
#[cfg(any(target_os = "macos", target_os = "ios"))]
use wkwebview::*;
#[cfg(target_os = "windows")]
pub(crate) mod webview2;
#[cfg(target_os = "windows")]
use self::webview2::*;
use crate::{application::dpi::PhysicalPosition, Result};
#[cfg(target_os = "windows")]
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller;
#[cfg(target_os = "windows")]
use windows::{Win32::Foundation::HWND, Win32::UI::WindowsAndMessaging::DestroyWindow};
use std::{borrow::Cow, path::PathBuf, rc::Rc};
pub use proxy::{ProxyConfig, ProxyEndpoint};
pub use url::Url;
#[cfg(target_os = "windows")]
use crate::application::platform::windows::WindowExtWindows;
use crate::application::{dpi::PhysicalSize, window::Window};
use http::{Request, Response as HttpResponse};
pub struct RequestAsyncResponder {
pub(crate) responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)>,
}
unsafe impl Send for RequestAsyncResponder {}
impl RequestAsyncResponder {
pub fn respond<T: Into<Cow<'static, [u8]>>>(self, response: HttpResponse<T>) {
let (parts, body) = response.into_parts();
(self.responder)(HttpResponse::from_parts(parts, body.into()))
}
}
pub struct WebViewAttributes {
pub user_agent: Option<String>,
pub visible: bool,
pub transparent: bool,
pub background_color: Option<RGBA>,
pub url: Option<Url>,
pub headers: Option<http::HeaderMap>,
pub zoom_hotkeys_enabled: bool,
pub html: Option<String>,
pub initialization_scripts: Vec<String>,
pub custom_protocols: Vec<(String, Box<dyn Fn(Request<Vec<u8>>, RequestAsyncResponder)>)>,
pub ipc_handler: Option<Box<dyn Fn(&Window, String)>>,
#[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>>,
pub navigation_handler: Option<Box<dyn Fn(String) -> bool>>,
pub download_started_handler: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool>>,
pub download_completed_handler: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
pub new_window_req_handler: Option<Box<dyn Fn(String) -> bool>>,
pub clipboard: bool,
pub devtools: bool,
pub accept_first_mouse: bool,
pub back_forward_navigation_gestures: bool,
pub document_title_changed_handler: Option<Box<dyn Fn(&Window, String)>>,
pub incognito: bool,
pub autoplay: bool,
pub on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent, String)>>,
pub proxy_config: Option<ProxyConfig>,
pub focused: bool,
}
impl Default for WebViewAttributes {
fn default() -> Self {
Self {
user_agent: None,
visible: true,
transparent: false,
background_color: None,
url: None,
headers: None,
html: None,
initialization_scripts: vec![],
custom_protocols: vec![],
ipc_handler: None,
file_drop_handler: None,
navigation_handler: None,
download_started_handler: None,
download_completed_handler: None,
new_window_req_handler: None,
clipboard: false,
#[cfg(debug_assertions)]
devtools: true,
#[cfg(not(debug_assertions))]
devtools: false,
zoom_hotkeys_enabled: false,
accept_first_mouse: false,
back_forward_navigation_gestures: false,
document_title_changed_handler: None,
incognito: false,
autoplay: true,
on_page_load_handler: None,
proxy_config: None,
focused: true,
}
}
}
#[cfg(windows)]
#[derive(Clone)]
pub(crate) struct PlatformSpecificWebViewAttributes {
additional_browser_args: Option<String>,
browser_accelerator_keys: bool,
theme: Option<Theme>,
https_scheme: bool,
}
#[cfg(windows)]
impl Default for PlatformSpecificWebViewAttributes {
fn default() -> Self {
Self {
additional_browser_args: None,
browser_accelerator_keys: true, theme: None,
https_scheme: false, }
}
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "macos",
target_os = "ios",
))]
#[derive(Default)]
pub(crate) struct PlatformSpecificWebViewAttributes;
#[cfg(target_os = "android")]
#[derive(Default)]
pub(crate) struct PlatformSpecificWebViewAttributes {
on_webview_created: Option<
Box<
dyn Fn(
prelude::Context,
) -> std::result::Result<(), tao::platform::android::ndk_glue::jni::errors::Error>
+ Send,
>,
>,
with_asset_loader: bool,
asset_loader_domain: Option<String>,
https_scheme: bool,
}
pub type RGBA = (u8, u8, u8, u8);
pub enum PageLoadEvent {
Started,
Finished,
}
pub struct WebViewBuilder<'a> {
pub webview: WebViewAttributes,
platform_specific: PlatformSpecificWebViewAttributes,
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;
#[allow(clippy::default_constructed_unit_structs)]
let platform_specific = PlatformSpecificWebViewAttributes::default();
Ok(Self {
webview,
web_context,
window,
platform_specific,
})
}
pub fn with_back_forward_navigation_gestures(mut self, gesture: bool) -> Self {
self.webview.back_forward_navigation_gestures = gesture;
self
}
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.webview.transparent = transparent;
self
}
pub fn with_background_color(mut self, background_color: RGBA) -> Self {
self.webview.background_color = Some(background_color);
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.webview.visible = visible;
self
}
pub fn with_autoplay(mut self, autoplay: bool) -> Self {
self.webview.autoplay = autoplay;
self
}
pub fn with_initialization_script(mut self, js: &str) -> Self {
if !js.is_empty() {
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(Request<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static,
{
self.webview.custom_protocols.push((
name,
Box::new(move |request, responder| {
let http_response = handler(request);
responder.respond(http_response);
}),
));
self
}
#[cfg(feature = "protocol")]
pub fn with_asynchronous_custom_protocol<F>(mut self, name: String, handler: F) -> Self
where
F: Fn(Request<Vec<u8>>, RequestAsyncResponder) + 'static,
{
self
.webview
.custom_protocols
.push((name, Box::new(handler)));
self
}
pub fn with_ipc_handler<F>(mut self, handler: F) -> Self
where
F: Fn(&Window, String) + 'static,
{
self.webview.ipc_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_and_headers(mut self, url: &str, headers: http::HeaderMap) -> Result<Self> {
self.webview.url = Some(url.parse()?);
self.webview.headers = Some(headers);
Ok(self)
}
pub fn with_url(mut self, url: &str) -> Result<Self> {
self.webview.url = Some(Url::parse(url)?);
self.webview.headers = None;
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 with_user_agent(mut self, user_agent: &str) -> Self {
self.webview.user_agent = Some(user_agent.to_string());
self
}
pub fn with_devtools(mut self, devtools: bool) -> Self {
self.webview.devtools = devtools;
self
}
pub fn with_hotkeys_zoom(mut self, zoom: bool) -> Self {
self.webview.zoom_hotkeys_enabled = zoom;
self
}
pub fn with_navigation_handler(mut self, callback: impl Fn(String) -> bool + 'static) -> Self {
self.webview.navigation_handler = Some(Box::new(callback));
self
}
pub fn with_download_started_handler(
mut self,
started_handler: impl FnMut(String, &mut PathBuf) -> bool + 'static,
) -> Self {
self.webview.download_started_handler = Some(Box::new(started_handler));
self
}
pub fn with_download_completed_handler(
mut self,
download_completed_handler: impl Fn(String, Option<PathBuf>, bool) + 'static,
) -> Self {
self.webview.download_completed_handler = Some(Rc::new(download_completed_handler));
self
}
pub fn with_clipboard(mut self, clipboard: bool) -> Self {
self.webview.clipboard = clipboard;
self
}
pub fn with_new_window_req_handler(
mut self,
callback: impl Fn(String) -> bool + 'static,
) -> Self {
self.webview.new_window_req_handler = Some(Box::new(callback));
self
}
pub fn with_accept_first_mouse(mut self, accept_first_mouse: bool) -> Self {
self.webview.accept_first_mouse = accept_first_mouse;
self
}
pub fn with_document_title_changed_handler(
mut self,
callback: impl Fn(&Window, String) + 'static,
) -> Self {
self.webview.document_title_changed_handler = Some(Box::new(callback));
self
}
pub fn with_incognito(mut self, incognito: bool) -> Self {
self.webview.incognito = incognito;
self
}
pub fn with_on_page_load_handler(
mut self,
handler: impl Fn(PageLoadEvent, String) + 'static,
) -> Self {
self.webview.on_page_load_handler = Some(Box::new(handler));
self
}
pub fn with_proxy_config(mut self, configuration: ProxyConfig) -> Self {
self.webview.proxy_config = Some(configuration);
self
}
pub fn with_focused(mut self, focused: bool) -> Self {
self.webview.focused = focused;
self
}
pub fn build(self) -> Result<WebView> {
let window = Rc::new(self.window);
let webview = InnerWebView::new(
window.clone(),
self.webview,
self.platform_specific,
self.web_context,
)?;
Ok(WebView { window, webview })
}
}
#[cfg(windows)]
pub trait WebViewBuilderExtWindows {
fn with_additional_browser_args<S: Into<String>>(self, additional_args: S) -> Self;
fn with_browser_accelerator_keys(self, enabled: bool) -> Self;
fn with_theme(self, theme: Theme) -> Self;
fn with_https_scheme(self, enabled: bool) -> Self;
}
#[cfg(windows)]
impl WebViewBuilderExtWindows for WebViewBuilder<'_> {
fn with_additional_browser_args<S: Into<String>>(mut self, additional_args: S) -> Self {
self.platform_specific.additional_browser_args = Some(additional_args.into());
self
}
fn with_browser_accelerator_keys(mut self, enabled: bool) -> Self {
self.platform_specific.browser_accelerator_keys = enabled;
self
}
fn with_theme(mut self, theme: Theme) -> Self {
self.platform_specific.theme = Some(theme);
self
}
fn with_https_scheme(mut self, enabled: bool) -> Self {
self.platform_specific.https_scheme = enabled;
self
}
}
#[cfg(target_os = "android")]
pub trait WebViewBuilderExtAndroid {
fn on_webview_created<
F: Fn(
prelude::Context<'_, '_>,
) -> std::result::Result<(), tao::platform::android::ndk_glue::jni::errors::Error>
+ Send
+ 'static,
>(
self,
f: F,
) -> Self;
#[cfg(feature = "protocol")]
fn with_asset_loader(self, protocol: String) -> Self;
fn with_https_scheme(self, enabled: bool) -> Self;
}
#[cfg(target_os = "android")]
impl WebViewBuilderExtAndroid for WebViewBuilder<'_> {
fn on_webview_created<
F: Fn(
prelude::Context<'_, '_>,
) -> std::result::Result<(), tao::platform::android::ndk_glue::jni::errors::Error>
+ Send
+ 'static,
>(
mut self,
f: F,
) -> Self {
self.platform_specific.on_webview_created = Some(Box::new(f));
self
}
#[cfg(feature = "protocol")]
fn with_asset_loader(mut self, protocol: String) -> Self {
self.webview.custom_protocols.push((
protocol.clone(),
Box::new(|_, api| {
api.respond(HttpResponse::builder().body(Vec::new()).unwrap());
}),
));
self.platform_specific.with_asset_loader = true;
self.platform_specific.asset_loader_domain = Some(format!("{}.assets", protocol));
self
}
fn with_https_scheme(mut self, enabled: bool) -> Self {
self.platform_specific.https_scheme = enabled;
self
}
}
pub struct WebView {
window: Rc<Window>,
webview: InnerWebView,
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
impl Drop for WebView {
fn drop(&mut self) {
unsafe {
use crate::application::platform::unix::WindowExtUnix;
use gtk::prelude::WidgetExtManual;
self.window().gtk_window().destroy();
}
}
}
#[cfg(target_os = "windows")]
impl Drop for WebView {
fn drop(&mut self) {
unsafe {
DestroyWindow(HWND(self.window.hwnd() as _));
}
}
}
impl WebView {
pub fn new(window: Window) -> Result<Self> {
WebViewBuilder::new(window)?.build()
}
pub fn window(&self) -> &Window {
&self.window
}
pub fn url(&self) -> Url {
self.webview.url()
}
pub fn evaluate_script(&self, js: &str) -> Result<()> {
self
.webview
.eval(js, None::<Box<dyn Fn(String) + Send + 'static>>)
}
pub fn evaluate_script_with_callback(
&self,
js: &str,
callback: impl Fn(String) + Send + 'static,
) -> Result<()> {
self.webview.eval(js, Some(callback))
}
pub fn print(&self) -> Result<()> {
self.webview.print();
Ok(())
}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn open_devtools(&self) {
self.webview.open_devtools();
}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn close_devtools(&self) {
self.webview.close_devtools();
}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn is_devtools_open(&self) -> bool {
self.webview.is_devtools_open()
}
pub fn inner_size(&self) -> PhysicalSize<u32> {
#[cfg(target_os = "macos")]
{
let scale_factor = self.window.scale_factor();
self.webview.inner_size(scale_factor)
}
#[cfg(not(target_os = "macos"))]
self.window.inner_size()
}
pub fn zoom(&self, scale_factor: f64) {
self.webview.zoom(scale_factor);
}
pub fn set_background_color(&self, background_color: RGBA) -> Result<()> {
self.webview.set_background_color(background_color)
}
pub fn load_url(&self, url: &str) {
self.webview.load_url(url)
}
pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) {
self.webview.load_url_with_headers(url, headers)
}
pub fn clear_all_browsing_data(&self) -> Result<()> {
self.webview.clear_all_browsing_data()
}
}
#[non_exhaustive]
#[derive(Debug, Serialize, Clone)]
pub enum FileDropEvent {
Hovered {
paths: Vec<PathBuf>,
position: PhysicalPosition<f64>,
},
Dropped {
paths: Vec<PathBuf>,
position: PhysicalPosition<f64>,
},
Cancelled,
}
pub fn webview_version() -> Result<String> {
platform_webview_version()
}
#[cfg(target_os = "windows")]
pub trait WebviewExtWindows {
fn controller(&self) -> ICoreWebView2Controller;
fn set_theme(&self, theme: Theme);
}
#[cfg(target_os = "windows")]
impl WebviewExtWindows for WebView {
fn controller(&self) -> ICoreWebView2Controller {
self.webview.controller.clone()
}
fn set_theme(&self, theme: Theme) {
self.webview.set_theme(theme)
}
}
#[cfg(target_os = "linux")]
pub trait WebviewExtUnix {
fn webview(&self) -> Rc<webkit2gtk::WebView>;
}
#[cfg(target_os = "linux")]
impl WebviewExtUnix for WebView {
fn webview(&self) -> Rc<webkit2gtk::WebView> {
self.webview.webview.clone()
}
}
#[cfg(target_os = "macos")]
pub trait WebviewExtMacOS {
fn webview(&self) -> cocoa::base::id;
fn manager(&self) -> cocoa::base::id;
fn ns_window(&self) -> cocoa::base::id;
}
#[cfg(target_os = "macos")]
impl WebviewExtMacOS for WebView {
fn webview(&self) -> cocoa::base::id {
self.webview.webview
}
fn manager(&self) -> cocoa::base::id {
self.webview.manager
}
fn ns_window(&self) -> cocoa::base::id {
self.webview.ns_window
}
}
#[cfg(target_os = "ios")]
pub trait WebviewExtIOS {
fn webview(&self) -> cocoa::base::id;
fn manager(&self) -> cocoa::base::id;
}
#[cfg(target_os = "ios")]
impl WebviewExtIOS for WebView {
fn webview(&self) -> cocoa::base::id {
self.webview.webview
}
fn manager(&self) -> cocoa::base::id {
self.webview.manager
}
}
#[cfg(target_os = "android")]
pub trait WebviewExtAndroid {
fn handle(&self) -> JniHandle;
}
#[cfg(target_os = "android")]
impl WebviewExtAndroid for WebView {
fn handle(&self) -> JniHandle {
JniHandle
}
}
#[derive(Debug, Clone, Copy)]
pub enum Theme {
Dark,
Light,
Auto,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_get_webview_version() {
if let Err(error) = webview_version() {
panic!("{}", error);
}
}
}