use super::*;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use block2::RcBlock;
use day_spec::NodeId;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObjectProtocol, ProtocolObject};
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send};
use objc2_app_kit::{NSApplication, NSEvent, NSMenu, NSMenuItem, NSResponder, NSView};
use objc2_foundation::{NSError, NSObject, NSString, NSURL, NSURLRequest};
use objc2_web_kit::{
WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate, WKWebView,
};
struct NavIvars {
emit: fn(NodeId, Event),
node: Cell<NodeId>,
inline_base: RefCell<Option<String>>,
external_links: Cell<bool>,
tabs: RefCell<Option<TabLinkLabels>>,
background: Cell<bool>,
link_item: RefCell<Option<Retained<NSMenuItem>>>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "DayWebNav"]
#[ivars = NavIvars]
struct WebNav;
unsafe impl NSObjectProtocol for WebNav {}
impl WebNav {
#[unsafe(method(openBackgroundTab:))]
fn open_background(&self, _sender: Option<&AnyObject>) {
let item = self.ivars().link_item.borrow().clone();
if let Some(item) = item {
self.ivars().background.set(true);
unsafe { if let Some(action) = item.action() {
NSApplication::sharedApplication(self.mtm()).sendAction_to_from(action, item.target().as_deref(), Some(&item));
} }
}
}
#[unsafe(method(webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:))]
fn new_window(&self, _web: &WKWebView, _config: &AnyObject, action: &WKNavigationAction, _features: &AnyObject) -> *mut AnyObject {
if self.ivars().tabs.borrow().is_some()
&& let Some(url) = unsafe { action.request() }.URL().and_then(|u|u.absoluteString()) {
(self.ivars().emit)(self.ivars().node.get(), new_tab_event(url.to_string(), self.ivars().background.replace(false)));
}
std::ptr::null_mut()
}
}
unsafe impl WKNavigationDelegate for WebNav {
#[unsafe(method(webView:didFinishNavigation:))]
fn did_finish(&self, web_view: &WKWebView, _navigation: Option<&WKNavigation>) {
if let Some(url) = current_url(web_view) {
(self.ivars().emit)(self.ivars().node.get(), Event::custom("webview:url", url));
}
}
#[unsafe(method(webView:decidePolicyForNavigationAction:decisionHandler:))]
fn decide_policy(
&self,
web_view: &WKWebView,
action: &WKNavigationAction,
handler: &block2::DynBlock<dyn Fn(WKNavigationActionPolicy)>,
) {
let frame = unsafe { action.targetFrame() };
let subframe = frame.as_ref().is_some_and(|f| !unsafe { f.isMainFrame() });
let url = unsafe { action.request() }
.URL()
.and_then(|u| u.absoluteString())
.map(|s| s.to_string())
.unwrap_or_default();
let activated = unsafe { action.navigationType() }
== objc2_web_kit::WKNavigationType::LinkActivated;
let modifiers: usize = unsafe { msg_send![action, modifierFlags] };
let button: isize = unsafe { msg_send![action, buttonNumber] };
if self.ivars().tabs.borrow().is_some() && !subframe &&
(frame.is_none() || (activated && (modifiers & (1 << 20) != 0 || button == 2))) {
let background = self.ivars().background.replace(false) ||
(activated && (modifiers & (1 << 20) != 0 || button == 2) && modifiers & (1 << 17) == 0);
(self.ivars().emit)(self.ivars().node.get(), new_tab_event(url, background));
handler.call((WKNavigationActionPolicy::Cancel,));
return;
}
let external = match &*self.ivars().inline_base.borrow() {
Some(base) => !subframe && !url.starts_with(base.as_str()) && url != "about:blank",
None => {
self.ivars().external_links.get()
&& super::external_document_link(
current_url(web_view).as_deref(),
&url,
activated,
frame.is_none(),
subframe,
)
}
};
let policy = if external {
(self.ivars().emit)(
self.ivars().node.get(),
Event::Custom {
tag: "webview:link",
num: super::LINK_REPORT,
text: url,
},
);
WKNavigationActionPolicy::Cancel
} else {
WKNavigationActionPolicy::Allow
};
handler.call((policy,));
}
}
);
define_class!(
#[unsafe(super(WKWebView, NSView, NSResponder, NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "DayTabbedWebView"]
struct TabWebView;
unsafe impl NSObjectProtocol for TabWebView {}
impl TabWebView {
#[unsafe(method(willOpenMenu:withEvent:))]
fn will_open_menu(&self, menu: &NSMenu, event: &NSEvent) {
unsafe { let _: () = msg_send![super(self), willOpenMenu: menu, withEvent: event]; }
let delegate = DELEGATES.with(|m|m.borrow().get(&(self as *const Self as usize)).cloned());
let Some(nav) = delegate else { return; };
let labels = nav.ivars().tabs.borrow().clone();
let Some(labels) = labels else { return; };
for item in menu.itemArray().iter() {
if item.tag() == 1 {
item.setTitle(&NSString::from_str(&labels.foreground));
*nav.ivars().link_item.borrow_mut() = Some(item.clone());
let background = NSMenuItem::new(self.mtm());
background.setTitle(&NSString::from_str(&labels.background));
unsafe {
background.setTarget(Some(&nav));
background.setAction(Some(objc2::sel!(openBackgroundTab:)));
}
menu.insertItem_atIndex(&background, menu.indexOfItem(&item) + 1);
break;
}
}
}
}
);
impl WebNav {
fn new(mtm: MainThreadMarker, node: NodeId, emit: fn(NodeId, Event)) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(NavIvars {
node: Cell::new(node),
emit,
inline_base: RefCell::new(None),
external_links: Cell::new(false),
tabs: RefCell::new(None),
background: Cell::new(false),
link_item: RefCell::new(None),
});
unsafe { msg_send![super(this), init] }
}
}
day_core::tls_group! {
static DELEGATES: RefCell<HashMap<usize, Retained<WebNav>>> = RefCell::new(HashMap::new());
static SESSIONS: RefCell<HashMap<u64, Retained<NSView>>> = RefCell::new(HashMap::new());
}
fn node_of(view: &Retained<NSView>) -> Option<NodeId> {
let key = (&**view as *const NSView) as usize;
DELEGATES.with(|m| m.borrow().get(&key).map(|nav| nav.ivars().node.get()))
}
fn current_url(web: &WKWebView) -> Option<String> {
let url = unsafe { web.URL() }?;
let s = url.absoluteString()?;
Some(s.to_string())
}
fn load_url(web: &WKWebView, url: &str) {
let ns = NSString::from_str(url);
let Some(nsurl) = NSURL::URLWithString(&ns) else {
return;
};
let req = NSURLRequest::requestWithURL(&nsurl);
let _ = unsafe { web.loadRequest(&req) };
}
fn page_url(dir: &std::path::Path, page: &str) -> objc2::rc::Retained<NSURL> {
let (path, tail) = super::split_page(page);
let url = NSURL::fileURLWithPath(&NSString::from_str(&dir.join(path).display().to_string()));
if tail.is_empty() {
return url;
}
NSURL::URLWithString_relativeToURL(&NSString::from_str(tail), Some(&url))
.and_then(|u| u.absoluteURL())
.unwrap_or(url)
}
pub(crate) fn make(
mtm: MainThreadMarker,
p: &WebProps,
id: NodeId,
emit: fn(NodeId, Event),
assets: Option<std::path::PathBuf>,
) -> Retained<NSView> {
if p.session != 0
&& let Some(view) = SESSIONS.with(|m| m.borrow().get(&p.session).cloned())
{
let key = (&*view as *const NSView) as usize;
DELEGATES.with(|m| {
if let Some(nav) = m.borrow().get(&key) {
nav.ivars().node.set(id);
nav.ivars().external_links.set(p.external_links);
*nav.ivars().tabs.borrow_mut() = p.tabs.clone();
*nav.ivars().tabs.borrow_mut() = p.tabs.clone();
}
});
return view;
}
let config =
super::resources_apple::configuration(p.resources.as_ref().map(ResourceProvider::id), mtm);
super::storage_apple::configure(&config, &p.profile);
let this = TabWebView::alloc(mtm).set_ivars(());
let web: Retained<TabWebView> = unsafe {
msg_send![super(this), initWithFrame: objc2_foundation::NSRect::ZERO, configuration: &*config]
};
let web: Retained<WKWebView> = web.into_super();
if p.transparent {
unsafe {
let no: *mut AnyObject = msg_send![objc2::class!(NSNumber), numberWithBool: false];
let key = NSString::from_str("drawsBackground");
let _: () = msg_send![&*web, setValue: no, forKey: &*key];
}
}
let nav = WebNav::new(mtm, id, emit);
nav.ivars().external_links.set(p.external_links);
*nav.ivars().tabs.borrow_mut() = p.tabs.clone();
unsafe {
let _: () = msg_send![&*web, setUIDelegate: &*nav];
}
unsafe { web.setNavigationDelegate(Some(ProtocolObject::from_ref(&*nav))) };
if !p.inline_root.is_empty() {
if let Some(dir) = assets
.clone()
.or_else(|| day_spec::resolve_asset_dir(&p.inline_root))
{
let site = NSURL::fileURLWithPath(&NSString::from_str(&dir.display().to_string()));
let read = match p
.inline_assets
.then(|| day_spec::resolve_asset_dir(""))
.flatten()
{
Some(assets) => {
NSURL::fileURLWithPath(&NSString::from_str(&assets.display().to_string()))
}
None => site.clone(),
};
let index = page_url(&dir, &p.inline_start);
if let Some(base) = site.absoluteString() {
*nav.ivars().inline_base.borrow_mut() = Some(base.to_string());
}
let _ = unsafe { web.loadFileURL_allowingReadAccessToURL(&index, &read) };
} else {
log::warn!(
"day-piece-webview: inline site {:?} not found in the staged assets",
p.inline_root
);
}
} else if !p.url.is_empty() {
if let Some(provider) = &p.resources {
*nav.ivars().inline_base.borrow_mut() = Some(provider.base_url());
}
load_url(&web, &p.url);
}
let view: Retained<NSView> = Retained::from(<WKWebView as AsRef<NSView>>::as_ref(&web));
DELEGATES.with(|m| {
m.borrow_mut()
.insert((view.as_ref() as *const NSView) as usize, nav)
});
if p.session != 0 {
SESSIONS.with(|m| m.borrow_mut().insert(p.session, view.clone()));
}
view
}
fn eval(web: &WKWebView, node: NodeId, req: u64, script: &str, emit: fn(NodeId, Event)) {
if super::storage_apple::request(web, script, move |text| {
emit(
node,
Event::Custom {
tag: "webview:eval",
num: req as f64,
text,
},
)
}) {
return;
}
let js = NSString::from_str(script);
let handler = RcBlock::new(move |result: *mut AnyObject, error: *mut NSError| {
let payload = if !result.is_null() {
let obj = unsafe { &*result };
obj.downcast_ref::<NSString>()
.map(|s| s.to_string())
.unwrap_or_else(|| engine_error("WebKitError", "non-string reply"))
} else if !error.is_null() {
let msg = unsafe { (*error).localizedDescription() }.to_string();
engine_error("WebKitError", &msg)
} else {
engine_error("WebKitError", "no result")
};
emit(
node,
Event::Custom {
tag: "webview:eval",
num: req as f64,
text: payload,
},
);
});
unsafe { web.evaluateJavaScript_completionHandler(&js, Some(&handler)) };
}
pub(crate) fn update(h: &Retained<NSView>, patch: &WebPatch) {
let Some(web) = h.downcast_ref::<WKWebView>() else {
return;
};
match patch {
WebPatch::Eval { req, script } => {
if let Some(node) = node_of(h) {
let key = (&**h as *const NSView) as usize;
if let Some(emit) = DELEGATES.with(|m| m.borrow().get(&key).map(|n| n.ivars().emit))
{
eval(web, node, *req, script, emit);
}
}
}
WebPatch::Load(url) => load_url(web, url),
WebPatch::Back => {
let _ = unsafe { web.goBack() };
}
WebPatch::Forward => {
let _ = unsafe { web.goForward() };
}
WebPatch::Stop => unsafe { web.stopLoading() },
WebPatch::ForceReload => {
let _ = unsafe { web.reloadFromOrigin() };
}
WebPatch::Reload => {
let _ = unsafe { web.reload() };
}
}
}
pub(crate) fn release(h: &Retained<NSView>) {
let key = (&**h as *const NSView) as usize;
let retained = SESSIONS.with(|m| {
m.borrow()
.values()
.any(|v| (&**v as *const NSView) as usize == key)
});
if retained {
return;
}
DELEGATES.with(|m| {
m.borrow_mut().remove(&key);
});
}