feat(webview): Add custom protocol (scheme) support
This is a basic implementation of the custom protocol, with built-in mimetype extraction from the content, backed by URI detection. To make it clear, I added an example and can be run with: ``` cargo run --example webview_custom_protocol --features webview ```
This commit is contained in:
parent
813f452deb
commit
c9d665963a
|
@ -26,6 +26,7 @@ objc_id = "0.1.1"
|
||||||
os_info = "3.0.1"
|
os_info = "3.0.1"
|
||||||
uuid = { version = "0.8", features = ["v4"], optional = true }
|
uuid = { version = "0.8", features = ["v4"], optional = true }
|
||||||
url = "2.1.1"
|
url = "2.1.1"
|
||||||
|
infer = "0.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
eval = "0.4"
|
eval = "0.4"
|
||||||
|
|
104
examples/webview_custom_protocol.rs
Normal file
104
examples/webview_custom_protocol.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
//! This example showcases setting up a basic application and window, setting up some views to
|
||||||
|
//! work with autolayout, and some basic ways to handle colors.
|
||||||
|
|
||||||
|
use cacao::webview::{WebView, WebViewConfig, WebViewDelegate};
|
||||||
|
|
||||||
|
use cacao::macos::{App, AppDelegate};
|
||||||
|
use cacao::macos::menu::{Menu, MenuItem};
|
||||||
|
use cacao::macos::toolbar::Toolbar;
|
||||||
|
use cacao::macos::window::{Window, WindowConfig, WindowDelegate, WindowToolbarStyle};
|
||||||
|
|
||||||
|
struct BasicApp {
|
||||||
|
window: Window<AppWindow>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppDelegate for BasicApp {
|
||||||
|
fn did_finish_launching(&self) {
|
||||||
|
App::activate();
|
||||||
|
self.window.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct WebViewInstance;
|
||||||
|
|
||||||
|
impl WebViewDelegate for WebViewInstance {
|
||||||
|
fn on_custom_protocol_request(&self, path: &str) -> Option<Vec<u8>> {
|
||||||
|
let requested_asset_path = path.replace("cacao://", "");
|
||||||
|
|
||||||
|
let index_html = r#"
|
||||||
|
<!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 🍫</h1>
|
||||||
|
<a href="/hello.html">Link</a>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
|
|
||||||
|
let link_html = r#"
|
||||||
|
<!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>Hello!</h1>
|
||||||
|
<a href="/index.html">Back home</a>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
|
|
||||||
|
return match requested_asset_path.as_str() {
|
||||||
|
"/hello.html" => Some(link_html.as_bytes().into()),
|
||||||
|
_ => Some(index_html.as_bytes().into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppWindow {
|
||||||
|
content: WebView<WebViewInstance>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppWindow {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut webview_config = WebViewConfig::default();
|
||||||
|
|
||||||
|
// register the protocol in the webview
|
||||||
|
webview_config.add_custom_protocol("cacao");
|
||||||
|
|
||||||
|
AppWindow {
|
||||||
|
content: WebView::with(webview_config, WebViewInstance::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_url(&self, url: &str) {
|
||||||
|
self.content.load_url(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowDelegate for AppWindow {
|
||||||
|
const NAME: &'static str = "WindowDelegate";
|
||||||
|
|
||||||
|
fn did_load(&mut self, window: Window) {
|
||||||
|
window.set_title("Browser Example");
|
||||||
|
window.set_autosave_name("CacaoBrowserExample");
|
||||||
|
window.set_minimum_content_size(400., 400.);
|
||||||
|
|
||||||
|
window.set_content_view(&self.content);
|
||||||
|
|
||||||
|
// load custom protocol
|
||||||
|
self.load_url("cacao://");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new("com.test.window", BasicApp {
|
||||||
|
window: Window::with(WindowConfig::default(), AppWindow::new())
|
||||||
|
}).run();
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::sync::Once;
|
use std::sync::Once;
|
||||||
|
use std::ptr::null;
|
||||||
|
|
||||||
use block::Block;
|
use block::Block;
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ use objc::runtime::{Class, Object, Sel};
|
||||||
use objc::{class, msg_send, sel, sel_impl};
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
use crate::foundation::{id, nil, YES, NO, NSString, NSArray, NSInteger};
|
use crate::foundation::{id, nil, YES, NO, NSString, NSArray, NSInteger};
|
||||||
use crate::webview::{WEBVIEW_DELEGATE_PTR, WebViewDelegate};
|
use crate::webview::{WEBVIEW_DELEGATE_PTR, WebViewDelegate, mimetype::MimeType};
|
||||||
use crate::webview::actions::{NavigationAction, NavigationResponse};//, OpenPanelParameters};
|
use crate::webview::actions::{NavigationAction, NavigationResponse};//, OpenPanelParameters};
|
||||||
//use crate::webview::enums::{NavigationPolicy, NavigationResponsePolicy};
|
//use crate::webview::enums::{NavigationPolicy, NavigationResponsePolicy};
|
||||||
use crate::utils::load;
|
use crate::utils::load;
|
||||||
|
@ -53,6 +54,39 @@ extern fn on_message<T: WebViewDelegate>(this: &Object, _: Sel, _: id, script_me
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fires when a custom protocol URI is requested from the underlying `WKWebView`.
|
||||||
|
extern fn start_url_scheme_task<T: WebViewDelegate>(this: &Object, _: Sel, _webview: id, task: id) {
|
||||||
|
let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let request: id = msg_send![task, request];
|
||||||
|
let url: id = msg_send![request, URL];
|
||||||
|
|
||||||
|
let uri = NSString::from_retained(msg_send![url, absoluteString]);
|
||||||
|
let uri_str = uri.to_str();
|
||||||
|
|
||||||
|
if let Some(content) = delegate.on_custom_protocol_request(uri_str) {
|
||||||
|
let mime = MimeType::parse(&content, uri_str);
|
||||||
|
let nsurlresponse: id = msg_send![class!(NSURLResponse), alloc];
|
||||||
|
let response: id = msg_send![nsurlresponse, initWithURL:url MIMEType:NSString::new(&mime)
|
||||||
|
expectedContentLength:content.len() textEncodingName:null::<c_void>()];
|
||||||
|
let _: () = msg_send![task, didReceiveResponse: response];
|
||||||
|
|
||||||
|
// Send data
|
||||||
|
let bytes = content.as_ptr() as *mut c_void;
|
||||||
|
let data: id = msg_send![class!(NSData), alloc];
|
||||||
|
let data: id = msg_send![data, initWithBytes:bytes length:content.len()];
|
||||||
|
let _: () = msg_send![task, didReceiveData: data];
|
||||||
|
|
||||||
|
// Finish
|
||||||
|
let () = msg_send![task, didFinish];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a custom protocol completed the task from the underlying `WKWebView`.
|
||||||
|
extern fn stop_url_scheme_task<T: WebViewDelegate>(_: &Object, _: Sel, _webview: id, _task: id) {}
|
||||||
|
|
||||||
/// Fires when deciding a navigation policy - i.e, should something be allowed or not.
|
/// Fires when deciding a navigation policy - i.e, should something be allowed or not.
|
||||||
extern fn decide_policy_for_action<T: WebViewDelegate>(this: &Object, _: Sel, _: id, action: id, handler: usize) {
|
extern fn decide_policy_for_action<T: WebViewDelegate>(this: &Object, _: Sel, _: id, action: id, handler: usize) {
|
||||||
let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR);
|
let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR);
|
||||||
|
@ -159,6 +193,10 @@ pub fn register_webview_delegate_class<T: WebViewDelegate>() -> *const Class {
|
||||||
// WKScriptMessageHandler
|
// WKScriptMessageHandler
|
||||||
decl.add_method(sel!(userContentController:didReceiveScriptMessage:), on_message::<T> as extern fn(&Object, _, _, id));
|
decl.add_method(sel!(userContentController:didReceiveScriptMessage:), on_message::<T> as extern fn(&Object, _, _, id));
|
||||||
|
|
||||||
|
// Custom protocol handler
|
||||||
|
decl.add_method(sel!(webView:startURLSchemeTask:), start_url_scheme_task::<T> as extern fn(&Object, Sel, id, id));
|
||||||
|
decl.add_method(sel!(webView:stopURLSchemeTask:), stop_url_scheme_task::<T> as extern fn(&Object, Sel, id, id));
|
||||||
|
|
||||||
// WKUIDelegate
|
// WKUIDelegate
|
||||||
decl.add_method(sel!(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:), alert::<T> as extern fn(&Object, _, _, id, _, _));
|
decl.add_method(sel!(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:), alert::<T> as extern fn(&Object, _, _, id, _, _));
|
||||||
decl.add_method(sel!(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:), run_open_panel::<T> as extern fn(&Object, _, _, id, _, usize));
|
decl.add_method(sel!(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:), run_open_panel::<T> as extern fn(&Object, _, _, id, _, usize));
|
||||||
|
|
|
@ -13,7 +13,8 @@ use crate::webview::enums::InjectAt;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct WebViewConfig {
|
pub struct WebViewConfig {
|
||||||
pub objc: Id<Object>,
|
pub objc: Id<Object>,
|
||||||
pub handlers: Vec<String>
|
pub handlers: Vec<String>,
|
||||||
|
pub protocols: Vec<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WebViewConfig {
|
impl Default for WebViewConfig {
|
||||||
|
@ -26,7 +27,8 @@ impl Default for WebViewConfig {
|
||||||
|
|
||||||
WebViewConfig {
|
WebViewConfig {
|
||||||
objc: config,
|
objc: config,
|
||||||
handlers: vec![]
|
handlers: vec![],
|
||||||
|
protocols: vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,12 @@ impl WebViewConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register the given protocol to the underlying `WKWebView`.
|
||||||
|
/// Example; protocol_name: `demo` will allow request to `demo://`
|
||||||
|
pub fn add_custom_protocol(&mut self, protocol_name: &str) {
|
||||||
|
self.protocols.push(protocol_name.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
/// Enables access to the underlying inspector view for `WKWebView`.
|
/// Enables access to the underlying inspector view for `WKWebView`.
|
||||||
pub fn enable_developer_extras(&mut self) {
|
pub fn enable_developer_extras(&mut self) {
|
||||||
let key = NSString::new("developerExtrasEnabled");
|
let key = NSString::new("developerExtrasEnabled");
|
||||||
|
|
73
src/webview/mimetype.rs
Normal file
73
src/webview/mimetype.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
const MIMETYPE_PLAIN: &str = "text/plain";
|
||||||
|
|
||||||
|
/// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers)
|
||||||
|
pub enum MimeType {
|
||||||
|
CSS,
|
||||||
|
CSV,
|
||||||
|
HTML,
|
||||||
|
ICO,
|
||||||
|
JS,
|
||||||
|
JSON,
|
||||||
|
JSONLD,
|
||||||
|
OCTETSTREAM,
|
||||||
|
RTF,
|
||||||
|
SVG,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for MimeType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let mime = match self {
|
||||||
|
MimeType::CSS => "text/css",
|
||||||
|
MimeType::CSV => "text/csv",
|
||||||
|
MimeType::HTML => "text/html",
|
||||||
|
MimeType::ICO => "image/vnd.microsoft.icon",
|
||||||
|
MimeType::JS => "text/javascript",
|
||||||
|
MimeType::JSON => "application/json",
|
||||||
|
MimeType::JSONLD => "application/ld+json",
|
||||||
|
MimeType::OCTETSTREAM => "application/octet-stream",
|
||||||
|
MimeType::RTF => "application/rtf",
|
||||||
|
MimeType::SVG => "image/svg+xml",
|
||||||
|
};
|
||||||
|
write!(f, "{}", mime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MimeType {
|
||||||
|
/// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType.
|
||||||
|
pub fn parse_from_uri(uri: &str) -> MimeType {
|
||||||
|
let suffix = uri.split('.').last();
|
||||||
|
match suffix {
|
||||||
|
Some("bin") => Self::OCTETSTREAM,
|
||||||
|
Some("css") => Self::CSS,
|
||||||
|
Some("csv") => Self::CSV,
|
||||||
|
Some("html") => Self::HTML,
|
||||||
|
Some("ico") => Self::ICO,
|
||||||
|
Some("js") => Self::JS,
|
||||||
|
Some("json") => Self::JSON,
|
||||||
|
Some("jsonld") => Self::JSONLD,
|
||||||
|
Some("rtf") => Self::RTF,
|
||||||
|
Some("svg") => Self::SVG,
|
||||||
|
// Assume HTML when a TLD is found for eg. `protocol:://example.com`
|
||||||
|
Some(_) => Self::HTML,
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||||
|
// using octet stream according to this:
|
||||||
|
None => Self::OCTETSTREAM,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// infer mimetype from content (or) URI if needed.
|
||||||
|
pub fn parse(content: &[u8], uri: &str) -> String {
|
||||||
|
let mime = match infer::get(&content) {
|
||||||
|
Some(info) => info.mime_type(),
|
||||||
|
None => MIMETYPE_PLAIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
if mime == MIMETYPE_PLAIN {
|
||||||
|
return Self::parse_from_uri(uri).to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
mime.to_string()
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ pub(crate) mod class;
|
||||||
use class::{register_webview_class, register_webview_delegate_class};
|
use class::{register_webview_class, register_webview_delegate_class};
|
||||||
//pub(crate) mod process_pool;
|
//pub(crate) mod process_pool;
|
||||||
|
|
||||||
|
mod mimetype;
|
||||||
mod traits;
|
mod traits;
|
||||||
pub use traits::WebViewDelegate;
|
pub use traits::WebViewDelegate;
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ fn allocate_webview(
|
||||||
unsafe {
|
unsafe {
|
||||||
// Not a fan of this, but we own it anyway, so... meh.
|
// Not a fan of this, but we own it anyway, so... meh.
|
||||||
let handlers = std::mem::take(&mut config.handlers);
|
let handlers = std::mem::take(&mut config.handlers);
|
||||||
|
let protocols = std::mem::take(&mut config.protocols);
|
||||||
let configuration = config.into_inner();
|
let configuration = config.into_inner();
|
||||||
|
|
||||||
if let Some(delegate) = &objc_delegate {
|
if let Some(delegate) = &objc_delegate {
|
||||||
|
@ -66,6 +68,11 @@ fn allocate_webview(
|
||||||
let name = NSString::new(&handler);
|
let name = NSString::new(&handler);
|
||||||
let _: () = msg_send![content_controller, addScriptMessageHandler:*delegate name:&*name];
|
let _: () = msg_send![content_controller, addScriptMessageHandler:*delegate name:&*name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for protocol in protocols {
|
||||||
|
let name = NSString::new(&protocol);
|
||||||
|
let _: () = msg_send![configuration, setURLSchemeHandler:*delegate forURLScheme:&*name];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let zero: CGRect = Rect::zero().into();
|
let zero: CGRect = Rect::zero().into();
|
||||||
|
|
|
@ -33,6 +33,11 @@ pub trait WebViewDelegate {
|
||||||
/// Note that at the moment, you really should handle bridging JSON/stringification yourself.
|
/// Note that at the moment, you really should handle bridging JSON/stringification yourself.
|
||||||
fn on_message(&self, _name: &str, _body: &str) {}
|
fn on_message(&self, _name: &str, _body: &str) {}
|
||||||
|
|
||||||
|
/// Called when a custom protocol URI is requested.
|
||||||
|
fn on_custom_protocol_request(&self, _uri: &str) -> Option<Vec<u8>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Given a callback handler, you can decide what policy should be taken for a given browser
|
/// Given a callback handler, you can decide what policy should be taken for a given browser
|
||||||
/// action. By default, this is `NavigationPolicy::Allow`.
|
/// action. By default, this is `NavigationPolicy::Allow`.
|
||||||
fn policy_for_navigation_action<F: Fn(NavigationPolicy)>(&self, _action: NavigationAction, handler: F) {
|
fn policy_for_navigation_action<F: Fn(NavigationPolicy)>(&self, _action: NavigationAction, handler: F) {
|
||||||
|
|
Loading…
Reference in a new issue