#![deny(unsafe_op_in_unsafe_fn)]
#![allow(clippy::incompatible_msrv)]
use core::cell::OnceCell;
use objc2::{
declare_class, msg_send_id,
mutability::MainThreadOnly,
rc::Retained,
runtime::{AnyObject, ProtocolObject, Sel},
sel, ClassType, DeclaredClass,
};
#[allow(deprecated)]
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSBackingStoreType,
NSBezelStyle, NSButton, NSColor, NSControl, NSControlTextEditingDelegate, NSLayoutAttribute,
NSMenu, NSMenuItem, NSStackView, NSStackViewDistribution, NSTextField, NSTextFieldDelegate,
NSTextView, NSUserInterfaceLayoutOrientation, NSWindow, NSWindowStyleMask,
};
use objc2_foundation::{
ns_string, MainThreadMarker, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect,
NSSize, NSURLRequest, NSURL,
};
use objc2_web_kit::{WKNavigation, WKNavigationDelegate, WKWebView};
macro_rules! idcell {
($name:ident => $this:expr) => {
$this.ivars().$name.set($name).expect(&format!(
"ivar should not already be initialized: `{}`",
stringify!($name)
));
};
($name:ident <= $this:expr) => {
#[rustfmt::skip]
let Some($name) = $this.ivars().$name.get() else {
unreachable!(
"ivar should be initialized: `{}`",
stringify!($name)
)
};
};
}
#[derive(Default)]
struct Ivars {
nav_url: OnceCell<Retained<NSTextField>>,
web_view: OnceCell<Retained<WKWebView>>,
window: OnceCell<Retained<NSWindow>>,
}
declare_class!(
struct Delegate;
unsafe impl ClassType for Delegate {
type Super = NSObject;
type Mutability = MainThreadOnly;
const NAME: &'static str = "Delegate";
}
impl DeclaredClass for Delegate {
type Ivars = Ivars;
}
unsafe impl NSObjectProtocol for Delegate {}
unsafe impl NSApplicationDelegate for Delegate {
#[method(applicationDidFinishLaunching:)]
#[allow(non_snake_case)]
unsafe fn applicationDidFinishLaunching(&self, _notification: &NSNotification) {
let mtm = MainThreadMarker::from(self);
let window = {
let content_rect = NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.));
let style = NSWindowStyleMask::Closable
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Titled;
let backing_store_type = NSBackingStoreType::NSBackingStoreBuffered;
let flag = false;
unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
mtm.alloc(),
content_rect,
style,
backing_store_type,
flag,
)
}
};
let web_view = {
let frame_rect = NSRect::ZERO;
unsafe { WKWebView::initWithFrame(mtm.alloc(), frame_rect) }
};
let nav_bar = {
let frame_rect = NSRect::ZERO;
let this = unsafe { NSStackView::initWithFrame(mtm.alloc(), frame_rect) };
unsafe {
this.setOrientation(NSUserInterfaceLayoutOrientation::Horizontal);
this.setAlignment(NSLayoutAttribute::Height);
this.setDistribution(NSStackViewDistribution::Fill);
this.setSpacing(0.);
}
this
};
let nav_buttons = {
let frame_rect = NSRect::ZERO;
let this = unsafe { NSStackView::initWithFrame(mtm.alloc(), frame_rect) };
unsafe {
this.setOrientation(NSUserInterfaceLayoutOrientation::Horizontal);
this.setAlignment(NSLayoutAttribute::Height);
this.setDistribution(NSStackViewDistribution::FillEqually);
this.setSpacing(0.);
}
this
};
let back_button = {
let title = ns_string!("back");
let target = Some::<&AnyObject>(&web_view);
let action = Some(sel!(goBack));
let this =
unsafe { NSButton::buttonWithTitle_target_action(title, target, action, mtm) };
#[allow(deprecated)]
unsafe {
this.setBezelStyle(NSBezelStyle::ShadowlessSquare)
};
this
};
let forward_button = {
let title = ns_string!("forward");
let target = Some::<&AnyObject>(&web_view);
let action = Some(sel!(goForward));
let this =
unsafe { NSButton::buttonWithTitle_target_action(title, target, action, mtm) };
#[allow(deprecated)]
unsafe {
this.setBezelStyle(NSBezelStyle::ShadowlessSquare)
};
this
};
unsafe {
nav_buttons.addArrangedSubview(&back_button);
nav_buttons.addArrangedSubview(&forward_button);
}
let nav_url = {
let frame_rect = NSRect::ZERO;
let this = unsafe { NSTextField::initWithFrame(mtm.alloc(), frame_rect) };
unsafe {
this.setDrawsBackground(true);
this.setBackgroundColor(Some(&NSColor::lightGrayColor()));
this.setTextColor(Some(&NSColor::blackColor()));
}
this
};
unsafe {
nav_bar.addArrangedSubview(&nav_buttons);
nav_bar.addArrangedSubview(&nav_url);
}
let content_view = {
let frame_rect = window.frame();
let this = unsafe { NSStackView::initWithFrame(mtm.alloc(), frame_rect) };
unsafe {
this.setOrientation(NSUserInterfaceLayoutOrientation::Vertical);
this.setAlignment(NSLayoutAttribute::Width);
this.setDistribution(NSStackViewDistribution::Fill);
this.setSpacing(0.);
}
this
};
unsafe {
content_view.addArrangedSubview(&nav_bar);
content_view.addArrangedSubview(&web_view);
}
unsafe {
let object = ProtocolObject::from_ref(self);
nav_url.setDelegate(Some(object));
let object = ProtocolObject::from_ref(self);
web_view.setNavigationDelegate(Some(object));
}
unsafe {
let menu = NSMenu::initWithTitle(mtm.alloc(), ns_string!(""));
let menu_app_item = NSMenuItem::initWithTitle_action_keyEquivalent(
mtm.alloc(),
ns_string!(""),
None,
ns_string!(""),
);
let menu_app_menu = NSMenu::initWithTitle(mtm.alloc(), ns_string!(""));
menu_app_menu.addItemWithTitle_action_keyEquivalent(
ns_string!("Quit"),
Some(sel!(terminate:)),
ns_string!("q"),
);
menu_app_item.setSubmenu(Some(&menu_app_menu));
menu.addItem(&menu_app_item);
let app = NSApplication::sharedApplication(mtm);
app.setMainMenu(Some(&menu));
}
window.setContentView(Some(&content_view));
window.center();
window.setTitle(ns_string!("browser example"));
window.makeKeyAndOrderFront(None);
unsafe {
let request = {
let url_string = ns_string!("https://google.com");
let url = NSURL::URLWithString(url_string).expect("URL should parse");
NSURLRequest::requestWithURL(&url)
};
web_view.loadRequest(&request);
}
idcell!(nav_url => self);
idcell!(web_view => self);
idcell!(window => self);
}
}
unsafe impl NSControlTextEditingDelegate for Delegate {
#[method(control:textView:doCommandBySelector:)]
#[allow(non_snake_case)]
unsafe fn control_textView_doCommandBySelector(
&self,
_control: &NSControl,
text_view: &NSTextView,
command_selector: Sel,
) -> bool {
idcell!(web_view <= self);
if command_selector == sel!(insertNewline:) {
if let Some(url) = unsafe { NSURL::URLWithString(&text_view.string()) } {
unsafe { web_view.loadRequest(&NSURLRequest::requestWithURL(&url)) };
return true.into();
}
}
false
}
}
unsafe impl NSTextFieldDelegate for Delegate {}
unsafe impl WKNavigationDelegate for Delegate {
#[method(webView:didFinishNavigation:)]
#[allow(non_snake_case)]
unsafe fn webView_didFinishNavigation(
&self,
web_view: &WKWebView,
_navigation: Option<&WKNavigation>,
) {
idcell!(nav_url <= self);
unsafe {
if let Some(url) = web_view.URL().and_then(|url| url.absoluteString()) {
nav_url.setStringValue(&url);
}
}
}
}
);
impl Delegate {
fn new(mtm: MainThreadMarker) -> Retained<Self> {
let this = mtm.alloc();
let this = this.set_ivars(Ivars::default());
unsafe { msg_send_id![super(this), init] }
}
}
fn main() {
let mtm = MainThreadMarker::new().unwrap();
let app = NSApplication::sharedApplication(mtm);
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
let delegate = Delegate::new(mtm);
let object = ProtocolObject::from_ref(&*delegate);
app.setDelegate(Some(object));
unsafe { app.run() };
}