use core::ffi::CStr;
use core::ptr;
use std::cell::{Cell, RefCell};
use std::path::PathBuf;
use std::sync::OnceLock;
use crate::{ShellBackend, ShellError};
use super::events::{RawEvent, enqueue};
use super::ffi::{self, Class, Id, Imp, ObjcBool, Pool, Sel, YES, value};
#[derive(Debug, Default)]
pub(super) struct Shared {
queue: RefCell<Vec<RawEvent>>,
text: RefCell<Vec<(usize, String)>>,
drops: RefCell<Vec<(usize, Vec<PathBuf>)>>,
keying: Cell<f64>,
marked: Cell<usize>,
cursors: RefCell<Vec<(usize, usize)>>,
}
impl Shared {
fn push(&self, event: RawEvent) {
enqueue(&mut self.queue.borrow_mut(), event);
}
pub(super) fn take_events(&self) -> Vec<RawEvent> {
core::mem::take(&mut self.queue.borrow_mut())
}
pub(super) fn push_text(&self, window: usize, text: String) {
self.text.borrow_mut().push((window, text));
}
pub(super) fn take_text(&self, window: usize) -> Option<String> {
let mut queued = self.text.borrow_mut();
let index = queued.iter().position(|(owner, _)| *owner == window)?;
Some(queued.remove(index).1)
}
pub(super) fn push_drop(&self, window: usize, paths: Vec<PathBuf>) {
self.drops.borrow_mut().push((window, paths));
}
pub(super) fn take_drop(&self, window: usize) -> Option<Vec<PathBuf>> {
let mut queued = self.drops.borrow_mut();
let index = queued.iter().position(|(owner, _)| *owner == window)?;
Some(queued.remove(index).1)
}
pub(super) fn keying(&self) -> f64 {
self.keying.get()
}
pub(super) fn set_keying(&self, seconds: f64) {
self.keying.set(seconds);
}
pub(super) fn marked(&self) -> usize {
self.marked.get()
}
pub(super) fn set_marked(&self, units: usize) {
self.marked.set(units);
}
pub(super) fn cursor_of(&self, window: usize) -> Option<usize> {
self.cursors
.borrow()
.iter()
.find(|(owner, _)| *owner == window)
.map(|(_, cursor)| *cursor)
}
pub(super) fn set_cursor(&self, window: usize, cursor: usize) {
let mut cursors = self.cursors.borrow_mut();
match cursors.iter_mut().find(|(owner, _)| *owner == window) {
Some(entry) => entry.1 = cursor,
None => cursors.push((window, cursor)),
}
}
pub(super) fn forget(&self, window: usize) {
self.queue
.borrow_mut()
.retain(|event| event.window() != Some(window));
self.text.borrow_mut().retain(|(owner, _)| *owner != window);
self.drops
.borrow_mut()
.retain(|(owner, _)| *owner != window);
self.cursors
.borrow_mut()
.retain(|(owner, _)| *owner != window);
}
}
thread_local! {
static DELEGATES: RefCell<Vec<(usize, *const Shared)>> = const { RefCell::new(Vec::new()) };
}
pub(super) fn with_shared<R>(delegate: Id, read: impl FnOnce(&Shared) -> R) -> Option<R> {
let shared = DELEGATES.with_borrow(|table| {
table
.iter()
.find(|(known, _)| *known == delegate as usize)
.map(|(_, shared)| *shared)
})?;
Some(read(unsafe { &*shared }))
}
pub(super) fn record(delegate: Id, event: RawEvent) {
with_shared(delegate, |shared| shared.push(event));
}
unsafe fn notified_window(notification: Id) -> usize {
if notification.is_null() {
return 0;
}
unsafe { ffi::msg(notification, ffi::sel(c"object")) as usize }
}
unsafe extern "C" fn window_should_close(delegate: Id, _cmd: Sel, sender: Id) -> ObjcBool {
record(
delegate,
RawEvent::CloseRequested {
window: sender as usize,
},
);
ffi::NO
}
unsafe extern "C" fn window_did_resize(delegate: Id, _cmd: Sel, notification: Id) {
let window = unsafe { notified_window(notification) };
record(delegate, RawEvent::Resized { window });
}
unsafe extern "C" fn window_did_change_backing(delegate: Id, _cmd: Sel, notification: Id) {
let window = unsafe { notified_window(notification) };
record(delegate, RawEvent::BackingChanged { window });
record(delegate, RawEvent::Resized { window });
}
unsafe extern "C" fn window_did_change_screen(delegate: Id, _cmd: Sel, notification: Id) {
let window = unsafe { notified_window(notification) };
record(delegate, RawEvent::BackingChanged { window });
}
unsafe extern "C" fn window_did_become_key(delegate: Id, _cmd: Sel, notification: Id) {
let window = unsafe { notified_window(notification) };
record(
delegate,
RawEvent::Focus {
window,
focused: true,
},
);
}
unsafe extern "C" fn window_did_resign_key(delegate: Id, _cmd: Sel, notification: Id) {
let window = unsafe { notified_window(notification) };
record(
delegate,
RawEvent::Focus {
window,
focused: false,
},
);
}
unsafe extern "C" fn window_will_close(delegate: Id, _cmd: Sel, notification: Id) {
let window = unsafe { notified_window(notification) };
record(delegate, RawEvent::Closed { window });
}
unsafe extern "C" fn screens_changed(delegate: Id, _cmd: Sel, _notification: Id) {
record(delegate, RawEvent::ScreensChanged);
}
const DELEGATE_CLASS: &CStr = c"CrcblWindowDelegate";
static DELEGATE: OnceLock<Result<usize, String>> = OnceLock::new();
fn delegate_class() -> Result<Class, ShellError> {
let outcome = DELEGATE.get_or_init(|| {
let Some(superclass) = ffi::class(c"NSObject") else {
return Err(
"the Objective-C runtime has no NSObject, so this image has no \
Foundation in it"
.to_string(),
);
};
let class = unsafe { ffi::objc_allocateClassPair(superclass, DELEGATE_CLASS.as_ptr(), 0) };
if class.is_null() {
let Some(existing) = ffi::class(DELEGATE_CLASS) else {
return Err(
"objc_allocateClassPair refused CrcblWindowDelegate and no class \
of that name exists"
.to_string(),
);
};
crcbl_core::log::debug!(
"the {DELEGATE_CLASS:?} class was already registered in this process"
);
return Ok(existing as usize);
}
let void_notification = c"v@:@";
let bool_notification = std::ffi::CString::new(format!("{}@:@", ffi::ENC_BOOL))
.expect("the encoding characters contain no NUL");
let methods: [(&CStr, Imp, &CStr); 8] = [
(
c"windowShouldClose:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id) -> ObjcBool, Imp>(
window_should_close,
)
},
bool_notification.as_c_str(),
),
(
c"windowDidResize:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(
window_did_resize,
)
},
void_notification,
),
(
c"windowDidChangeBackingProperties:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(
window_did_change_backing,
)
},
void_notification,
),
(
c"windowDidChangeScreen:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(
window_did_change_screen,
)
},
void_notification,
),
(
c"windowDidBecomeKey:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(
window_did_become_key,
)
},
void_notification,
),
(
c"windowDidResignKey:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(
window_did_resign_key,
)
},
void_notification,
),
(
c"windowWillClose:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(
window_will_close,
)
},
void_notification,
),
(
SCREENS_CHANGED,
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(screens_changed)
},
void_notification,
),
];
for (name, imp, types) in methods {
let added = unsafe { ffi::class_addMethod(class, ffi::sel(name), imp, types.as_ptr()) };
if added == ffi::NO {
return Err(format!(
"class_addMethod refused {name:?} on CrcblWindowDelegate"
));
}
}
let protocol = unsafe { ffi::objc_getProtocol(c"NSWindowDelegate".as_ptr()) };
if !protocol.is_null() {
unsafe { ffi::class_addProtocol(class, protocol) };
}
unsafe { ffi::objc_registerClassPair(class) };
Ok(class as usize)
});
match outcome {
Ok(class) => Ok(*class as Class),
Err(detail) => Err(ShellError::Connect {
backend: ShellBackend::AppKit,
detail: detail.clone(),
}),
}
}
const SCREENS_CHANGED: &CStr = c"crcblScreenParametersChanged:";
#[derive(Debug)]
pub(super) struct Delegate {
object: Id,
}
impl Delegate {
pub(super) fn new(shared: *const Shared) -> Result<Self, ShellError> {
let class = delegate_class()?;
let object = unsafe {
let allocated = ffi::msg(class, ffi::sel(c"alloc"));
ffi::msg(allocated, ffi::sel(c"init"))
};
if object.is_null() {
return Err(ShellError::Connect {
backend: ShellBackend::AppKit,
detail: "[[CrcblWindowDelegate alloc] init] returned nil".to_string(),
});
}
DELEGATES.with_borrow_mut(|table| table.push((object as usize, shared)));
unsafe {
if let Some(centre_class) = ffi::class(c"NSNotificationCenter") {
let centre = ffi::msg(centre_class, ffi::sel(c"defaultCenter"));
let add: unsafe extern "C" fn(Id, Sel, Id, Sel, Id, Id) = ffi::msg_send();
add(
centre,
ffi::sel(c"addObserver:selector:name:object:"),
object,
ffi::sel(SCREENS_CHANGED),
ffi::NSApplicationDidChangeScreenParametersNotification,
ptr::null_mut(),
);
}
}
Ok(Self { object })
}
pub(super) const fn object(&self) -> Id {
self.object
}
}
impl Drop for Delegate {
fn drop(&mut self) {
unsafe {
if let Some(centre_class) = ffi::class(c"NSNotificationCenter") {
let centre = ffi::msg(centre_class, ffi::sel(c"defaultCenter"));
ffi::msg1_void(centre, ffi::sel(c"removeObserver:"), self.object);
}
}
DELEGATES
.with_borrow_mut(|table| table.retain(|(known, _)| *known != self.object as usize));
unsafe { ffi::msg_void(self.object, ffi::sel(c"release")) };
}
}
#[must_use]
pub(super) fn is_main_thread() -> bool {
let Some(class) = ffi::class(c"NSThread") else {
return false;
};
unsafe { ffi::msg_bool(class, ffi::sel(c"isMainThread")) }
}
pub(super) fn require_main_thread() -> Result<(), ShellError> {
if is_main_thread() {
return Ok(());
}
Err(ShellError::Backend(
"the AppKit backend must be opened on the process's main thread: AppKit asserts on it \
and raises an Objective-C exception, and the WindowServer's event source is attached \
to the main run loop, so a shell created anywhere else would either abort or open a \
window that never receives an event"
.to_string(),
))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Bootstrap {
pub app: Id,
pub policy_accepted: bool,
}
static LAUNCHED: OnceLock<bool> = OnceLock::new();
pub(super) fn bootstrap() -> Result<Bootstrap, ShellError> {
let _pool = Pool::push();
let Some(class) = ffi::class(c"NSApplication") else {
return Err(ShellError::Connect {
backend: ShellBackend::AppKit,
detail: "the Objective-C runtime has no NSApplication, so this image has no AppKit \
in it"
.to_string(),
});
};
let app = unsafe { ffi::msg(class, ffi::sel(c"sharedApplication")) };
if app.is_null() {
return Err(ShellError::Connect {
backend: ShellBackend::AppKit,
detail: "[NSApplication sharedApplication] returned nil, which is what a process \
with no WindowServer session gets"
.to_string(),
});
}
let policy_accepted = *LAUNCHED.get_or_init(|| {
let accepted = unsafe {
let set: unsafe extern "C" fn(Id, Sel, isize) -> ObjcBool = ffi::msg_send();
set(
app,
ffi::sel(c"setActivationPolicy:"),
value::ACTIVATION_POLICY_REGULAR,
) != ffi::NO
};
if !accepted {
crcbl_core::log::warn!(
"setActivationPolicy:Regular was refused; this process cannot become \
frontmost, so its windows will open behind other applications and will not \
take the keyboard"
);
}
unsafe { ffi::msg_void(app, ffi::sel(c"finishLaunching")) };
accepted
});
activate(app);
Ok(Bootstrap {
app,
policy_accepted,
})
}
fn activate(app: Id) {
unsafe {
let modern = ffi::sel(c"activate");
if ffi::responds_to(app, modern) {
ffi::msg_void(app, modern);
} else {
let legacy: unsafe extern "C" fn(Id, Sel, ObjcBool) = ffi::msg_send();
legacy(app, ffi::sel(c"activateIgnoringOtherApps:"), YES);
}
}
}