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"
|
||||
uuid = { version = "0.8", features = ["v4"], optional = true }
|
||||
url = "2.1.1"
|
||||
infer = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
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::sync::Once;
|
||||
use std::ptr::null;
|
||||
|
||||
use block::Block;
|
||||
|
||||
|
@ -12,7 +13,7 @@ use objc::runtime::{Class, Object, Sel};
|
|||
use objc::{class, msg_send, sel, sel_impl};
|
||||
|
||||
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::enums::{NavigationPolicy, NavigationResponsePolicy};
|
||||
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.
|
||||
extern fn decide_policy_for_action<T: WebViewDelegate>(this: &Object, _: Sel, _: id, action: id, handler: usize) {
|
||||
let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR);
|
||||
|
@ -159,6 +193,10 @@ pub fn register_webview_delegate_class<T: WebViewDelegate>() -> *const Class {
|
|||
// WKScriptMessageHandler
|
||||
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
|
||||
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));
|
||||
|
|
|
@ -13,7 +13,8 @@ use crate::webview::enums::InjectAt;
|
|||
#[derive(Debug)]
|
||||
pub struct WebViewConfig {
|
||||
pub objc: Id<Object>,
|
||||
pub handlers: Vec<String>
|
||||
pub handlers: Vec<String>,
|
||||
pub protocols: Vec<String>
|
||||
}
|
||||
|
||||
impl Default for WebViewConfig {
|
||||
|
@ -26,7 +27,8 @@ impl Default for WebViewConfig {
|
|||
|
||||
WebViewConfig {
|
||||
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`.
|
||||
pub fn enable_developer_extras(&mut self) {
|
||||
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};
|
||||
//pub(crate) mod process_pool;
|
||||
|
||||
mod mimetype;
|
||||
mod traits;
|
||||
pub use traits::WebViewDelegate;
|
||||
|
||||
|
@ -50,6 +51,7 @@ fn allocate_webview(
|
|||
unsafe {
|
||||
// Not a fan of this, but we own it anyway, so... meh.
|
||||
let handlers = std::mem::take(&mut config.handlers);
|
||||
let protocols = std::mem::take(&mut config.protocols);
|
||||
let configuration = config.into_inner();
|
||||
|
||||
if let Some(delegate) = &objc_delegate {
|
||||
|
@ -66,6 +68,11 @@ fn allocate_webview(
|
|||
let name = NSString::new(&handler);
|
||||
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();
|
||||
|
|
|
@ -33,6 +33,11 @@ pub trait WebViewDelegate {
|
|||
/// Note that at the moment, you really should handle bridging JSON/stringification yourself.
|
||||
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
|
||||
/// action. By default, this is `NavigationPolicy::Allow`.
|
||||
fn policy_for_navigation_action<F: Fn(NavigationPolicy)>(&self, _action: NavigationAction, handler: F) {
|
||||
|
|
Loading…
Reference in a new issue