#![allow(non_snake_case, non_upper_case_globals)]
use {Handler, Options, Window};
use cocoa::appkit::{
NSApplicationActivationPolicyRegular,
NSApplicationTerminateReply,
NSBackingStoreBuffered,
NSWindowStyleMask,
};
use cocoa::base::{BOOL, YES, NO, id, nil};
use cocoa::foundation::{
NSInteger,
NSRect,
NSPoint,
NSSize,
NSUInteger,
};
use dispatch::Queue;
use objc::{Encode, Encoding};
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use std::{process, ptr};
use std::borrow::Cow;
use std::cell::Cell;
use std::ffi::CStr;
use std::os::raw::{c_char, c_int, c_void};
thread_local! {
static WEBVIEW: Cell<*const Object> = Cell::new(ptr::null());
}
pub fn start<H: Handler>(opts: Options<H>) -> ! {
unsafe {
let NSApplication = Class::get("NSApplication").unwrap();
let NSAutoreleasePool = Class::get("NSAutoreleasePool").unwrap();
let NSObject = Class::get("NSObject").unwrap();
let NSWindow = Class::get("NSWindow").unwrap();
let RKAppDelegate = register_delegate(NSObject);
let pool: id = msg_send![NSAutoreleasePool, alloc];
let app: id = msg_send![NSApplication, sharedApplication];
let delegate: id = msg_send![RKAppDelegate, alloc];
let pool: id = msg_send![pool, init];
let delegate: id = msg_send![delegate, init];
add_menus(app);
let html = NSString::new(opts.html);
let min_size = opts.minimum_size.unwrap_or((opts.width, opts.height));
let spw = Speedwagon {
data: Box::into_raw(Box::new(opts.handler)) as *mut c_void,
message: { extern "C" fn message<H: Handler>(h: *mut c_void, s: &str) {
unsafe {
let h = h as *mut H;
(*h).message(Window::new(), s)
}
}; message::<H> },
suspend: { extern "C" fn suspend<H: Handler>(h: *mut c_void) {
unsafe {
let h = h as *mut H;
(*h).suspend(Window::new())
}
}; suspend::<H> },
html: html.raw,
frame: NSRect::new(
NSPoint::new(0.0, 0.0),
NSSize::new(opts.width as f64, opts.height as f64)
),
min_size: NSSize::new(min_size.0 as f64, min_size.1 as f64),
fullscreen: opts.fullscreen,
};
msg_send![html.raw, retain]; ptr::write((*delegate).get_mut_ivar("spw"), spw);
msg_send![NSWindow, setAllowsAutomaticWindowTabbing: NO];
msg_send![app, setDelegate:delegate];
msg_send![app, setActivationPolicy:NSApplicationActivationPolicyRegular];
msg_send![app, run];
WEBVIEW.with(|cell| {
let old = cell.replace(ptr::null());
if !old.is_null() {
msg_send![old, release];
}
});
msg_send![delegate, release];
msg_send![pool, release];
}
process::exit(0);
}
pub fn load(html: &str) {
WEBVIEW.with(|cell| {
let webview = cell.get();
if !webview.is_null() {
unsafe {
msg_send![webview,
loadHTMLString: NSString::new(html)
baseURL: nil];
}
}
})
}
pub fn eval(js: &str) {
WEBVIEW.with(|cell| {
let webview = cell.get();
if !webview.is_null() {
unsafe {
msg_send![webview,
evaluateJavaScript:NSString::new(js)
completionHandler:nil];
}
}
})
}
pub fn dispatch<F: FnOnce(Window) + Send + 'static>(f: F) {
Queue::main().async(move || {
f(unsafe { Window::new() });
});
}
unsafe fn register_delegate(NSObject: &Class) -> &'static Class {
let mut decl = ClassDecl::new("RKAppDelegate", NSObject).unwrap();
decl.add_method(sel!(applicationDidFinishLaunching:), application_did_finish_launching as extern "C" fn(&mut Object, Sel, *mut Object));
decl.add_method(sel!(applicationShouldTerminateAfterLastWindowClosed:), should_app_kys as extern "C" fn(&mut Object, Sel, *mut Object) -> BOOL);
decl.add_method(sel!(applicationShouldTerminate:), app_will_kys as extern "C" fn(&mut Object, Sel, *mut Object) -> c_int);
decl.add_method(sel!(userContentController:didReceiveScriptMessage:), receive_message as extern "C" fn(&mut Object, Sel, *mut Object, *mut Object));
decl.add_method(sel!(observeValueForKeyPath:ofObject:change:context:), handle_change as extern "C" fn(&mut Object, Sel, *mut Object, *mut Object, *mut Object, *mut c_void));
decl.add_ivar::<Speedwagon>("spw");
decl.register()
}
extern "C" fn application_did_finish_launching(this: &mut Object, _: Sel, _: *mut Object) {
unsafe {
let NSWindow = Class::get("NSWindow").unwrap();
let WKWebView = Class::get("WKWebView").unwrap();
let WKUserScript = Class::get("WKUserScript").unwrap();
let WKWebViewConfiguration = Class::get("WKWebViewConfiguration").unwrap();
let str_x = NSString::new("x");
let str_script = NSString::new("\
window.tether = function (s) { window.webkit.messageHandlers.x.postMessage(s); };\
document.addEventListener('contextmenu', function (e) { e.preventDefault(); return false; });\
");
let spw = this.get_ivar::<Speedwagon>("spw");
let window: id = msg_send![NSWindow, alloc];
let webview: id = msg_send![WKWebView, alloc];
let config: id = msg_send![WKWebViewConfiguration, new];
let script: id = msg_send![WKUserScript, alloc];
let cc: id = msg_send![config, userContentController];
let script: id = msg_send![script,
initWithSource:str_script
injectionTime:0 as NSInteger
forMainFrameOnly:YES];
msg_send![cc, addScriptMessageHandler:&*this name:str_x];
msg_send![cc, addUserScript:script];
let webview: id = msg_send![webview,
initWithFrame:NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0))
configuration:config];
msg_send![webview,
addObserver:&*this
forKeyPath:NSString::new("title")
options:0 as c_int
context:ptr::null_mut::<c_void>()];
msg_send![webview,
loadHTMLString:(*spw).html
baseURL:nil];
WEBVIEW.with(|cell| {
let old = cell.replace(webview);
if !old.is_null() {
msg_send![old, release];
}
});
let mut window_style = NSWindowStyleMask::NSTitledWindowMask
| NSWindowStyleMask::NSClosableWindowMask
| NSWindowStyleMask::NSMiniaturizableWindowMask
| NSWindowStyleMask::NSResizableWindowMask;
if (*spw).fullscreen {
window_style |= NSWindowStyleMask::NSFullScreenWindowMask;
}
let frame: NSRect = (*spw).frame;
let window: id = msg_send![window,
initWithContentRect:frame
styleMask:window_style
backing:NSBackingStoreBuffered
defer:NO];
msg_send![window, setContentMinSize:(*spw).min_size];
msg_send![window, setContentView:webview];
msg_send![window, center];
msg_send![window, makeKeyAndOrderFront:nil];
msg_send![config, release];
msg_send![script, release];
}
}
extern "C" fn app_will_kys(this: &mut Object, _: Sel, _: *mut Object) -> c_int {
unsafe {
let handler = this.get_mut_ivar::<Speedwagon>("spw");
((*handler).suspend)((*handler).data);
}
NSApplicationTerminateReply::NSTerminateNow as c_int
}
extern "C" fn should_app_kys(_: &mut Object, _: Sel, _: *mut Object) -> BOOL {
YES
}
extern "C" fn receive_message(this: &mut Object, _: Sel, _: *mut Object, msg: *mut Object) {
unsafe {
let NSString = Class::get("NSString").unwrap();
let handler = this.get_mut_ivar::<Speedwagon>("spw");
let body: id = msg_send![msg, body];
if YES == msg_send![body, isKindOfClass:NSString] {
let s = NSString::from_raw(body);
((*handler).message)((*handler).data, &s.as_str());
}
}
}
extern "C" fn handle_change(_: &mut Object, _: Sel, _: *mut Object, obj: *mut Object, _: *mut Object, _: *mut c_void) {
unsafe {
let title: id = msg_send![obj, title];
let window: id = msg_send![obj, window];
msg_send![window, setTitle:title];
}
}
struct NSString { raw: id }
impl NSString {
fn new(s: &str) -> Self {
let NSString = Class::get("NSString").unwrap();
let raw = unsafe {
let raw: id = msg_send![NSString, alloc];
let raw: id = msg_send![raw,
initWithBytes:s.as_ptr()
length:s.len()
encoding:4 as NSUInteger];
raw
};
Self { raw }
}
unsafe fn concat(&self, other: id) -> Self {
let raw: id = msg_send![self.raw, stringByAppendingString:other];
Self { raw }
}
unsafe fn from_raw(raw: id) -> Self {
msg_send![raw, retain];
Self { raw }
}
fn as_str(&self) -> Cow<str> {
unsafe {
let ptr: *const c_char = msg_send![self.raw, UTF8String];
CStr::from_ptr(ptr).to_string_lossy()
}
}
}
impl Clone for NSString {
fn clone(&self) -> Self {
unsafe {
msg_send![self.raw, retain];
}
Self { raw: self.raw }
}
}
impl Drop for NSString {
fn drop(&mut self) {
unsafe {
msg_send![self.raw, release];
}
}
}
#[repr(C)]
struct Speedwagon {
data: *mut c_void,
message: extern "C" fn(*mut c_void, &str),
suspend: extern "C" fn(*mut c_void),
fullscreen: bool,
frame: NSRect,
min_size: NSSize,
html: id,
}
unsafe impl Encode for Speedwagon {
fn encode() -> Encoding { unsafe { Encoding::from_str("{spw=^v^?^?^?}") } }
}
unsafe fn add_menus(app: id) {
let NSMenu = Class::get("NSMenu").unwrap();
let NSMenuItem = Class::get("NSMenuItem").unwrap();
let NSProcessInfo = Class::get("NSProcessInfo").unwrap();
let process: id = msg_send![NSProcessInfo, processInfo];
let appname: id = msg_send![process, processName];
let str_edit = NSString::new("Edit");
let str_window = NSString::new("Window");
let str_empty = NSString::new("");
let str_mainmenu = NSString::new("MainMenu");
let str_hide_others = NSString::new("Hide Others");
let str_close = NSString::new("Close");
let str_show_all = NSString::new("Show All");
let str_minimize = NSString::new("Minimize");
let str_toggle_fullscreen = NSString::new("Toggle Full Screen");
let str_select_all = NSString::new("Select All");
let str_cut = NSString::new("Cut");
let str_copy = NSString::new("Copy");
let str_paste = NSString::new("Paste");
let str_delete = NSString::new("Delete");
let str_undo = NSString::new("Undo");
let str_redo = NSString::new("Redo");
let str_hide_appname = NSString::new("Hide ").concat(appname);
let str_quit_appname = NSString::new("Quit ").concat(appname);
let str_a = NSString::new("a");
let str_c = NSString::new("c");
let str_f = NSString::new("f");
let str_h = NSString::new("h");
let str_m = NSString::new("m");
let str_q = NSString::new("q");
let str_v = NSString::new("v");
let str_w = NSString::new("w");
let str_x = NSString::new("x");
let str_z = NSString::new("z");
let menu: id = msg_send![NSMenu, alloc];
let menu: id = msg_send![menu, initWithTitle:str_mainmenu.raw];
let item_app: id = msg_send![menu, addItemWithTitle:appname action:nil keyEquivalent:str_empty.raw];
let item_edit: id = msg_send![menu, addItemWithTitle:str_edit.raw action:nil keyEquivalent:str_empty.raw];
let item_window: id = msg_send![menu, addItemWithTitle:str_window.raw action:nil keyEquivalent:str_empty.raw];
let submenu: id = msg_send![NSMenu, alloc];
let submenu: id = msg_send![submenu, initWithTitle:appname];
let _: id = msg_send![submenu,
addItemWithTitle:str_hide_appname.raw
action:sel!(hide:)
keyEquivalent:str_h.raw];
let item: id = msg_send![submenu,
addItemWithTitle:str_hide_others.raw
action:sel!(hideOtherApplications:)
keyEquivalent:str_h.raw];
msg_send![item, setKeyEquivalentModifierMask:(3 as c_int) << 19];
let _: id = msg_send![submenu,
addItemWithTitle:str_show_all.raw
action:sel!(unhideAllApplications:)
keyEquivalent:str_empty.raw];
let sep: id = msg_send![NSMenuItem, separatorItem];
msg_send![submenu, addItem:sep];
let _: id = msg_send![submenu,
addItemWithTitle:str_quit_appname.raw
action:sel!(terminate:)
keyEquivalent:str_q.raw];
msg_send![menu, setSubmenu:submenu forItem:item_app];
msg_send![submenu, release];
let submenu: id = msg_send![NSMenu, alloc];
let submenu: id = msg_send![submenu, initWithTitle:str_edit.raw];
let _: id = msg_send![submenu,
addItemWithTitle:str_undo.raw
action:sel!(undo:)
keyEquivalent:str_z.raw];
let item: id = msg_send![submenu,
addItemWithTitle:str_redo.raw
action:sel!(redo:)
keyEquivalent:str_z.raw];
msg_send![item, setKeyEquivalentModifierMask:(9 as c_int) << 17];
let sep: id = msg_send![NSMenuItem, separatorItem];
msg_send![submenu, addItem:sep];
let _: id = msg_send![submenu,
addItemWithTitle:str_cut.raw
action:sel!(cut:)
keyEquivalent:str_x.raw];
let _: id = msg_send![submenu,
addItemWithTitle:str_copy.raw
action:sel!(copy:)
keyEquivalent:str_c.raw];
let _: id = msg_send![submenu,
addItemWithTitle:str_paste.raw
action:sel!(paste:)
keyEquivalent:str_v.raw];
let _: id = msg_send![submenu,
addItemWithTitle:str_delete.raw
action:sel!(delete:)
keyEquivalent:str_empty.raw];
let _: id = msg_send![submenu,
addItemWithTitle:str_select_all.raw
action:sel!(selectAll:)
keyEquivalent:str_a.raw];
msg_send![menu, setSubmenu:submenu forItem:item_edit];
msg_send![submenu, release];
let submenu: id = msg_send![NSMenu, alloc];
let submenu: id = msg_send![submenu, initWithTitle:str_window.raw];
let _: id = msg_send![submenu,
addItemWithTitle:str_close.raw
action:sel!(close)
keyEquivalent:str_w.raw];
let _: id = msg_send![submenu,
addItemWithTitle:str_minimize.raw
action:sel!(miniaturize:)
keyEquivalent:str_m.raw];
let item: id = msg_send![submenu,
addItemWithTitle:str_toggle_fullscreen.raw
action:sel!(toggleFullScreen:)
keyEquivalent:str_f.raw];
msg_send![item, setKeyEquivalentModifierMask:(5 as c_int) << 18];
msg_send![menu, setSubmenu:submenu forItem:item_window];
msg_send![app, setWindowsMenu:submenu];
msg_send![submenu, release];
msg_send![app, setMainMenu:menu];
msg_send![menu, release];
}