use core::ffi::{CStr, c_char};
use crate::{CursorIcon, MimeType};
use super::ffi::{self, Id, NSSize, NSUInteger, Pool, Sel, style};
use super::pasteboard;
use super::pointer;
use super::view;
#[link(name = "objc")]
unsafe extern "C" {
fn object_getClassName(object: Id) -> *const c_char;
}
pub fn every_cursor_shape_exists() -> Result<(), String> {
let _pool = Pool::push();
let Some(class) = ffi::class(c"NSCursor") else {
return Err("the Objective-C runtime has no NSCursor, so this image has no AppKit".into());
};
for shape in [
CursorIcon::Default,
CursorIcon::Pointer,
CursorIcon::Text,
CursorIcon::Crosshair,
CursorIcon::Wait,
CursorIcon::Progress,
CursorIcon::NotAllowed,
CursorIcon::Grab,
CursorIcon::Grabbing,
CursorIcon::Move,
CursorIcon::ResizeNorthSouth,
CursorIcon::ResizeEastWest,
CursorIcon::ResizeNorthEastSouthWest,
CursorIcon::ResizeNorthWestSouthEast,
CursorIcon::ResizeColumn,
CursorIcon::ResizeRow,
] {
let selector = pointer::cursor_selector(shape);
if !unsafe { ffi::responds_to(class, ffi::sel(selector)) } {
return Err(format!(
"+[NSCursor {selector:?}] does not exist, so {shape:?} would be nil"
));
}
let cursor = unsafe { ffi::msg(class, ffi::sel(selector)) };
if cursor.is_null() {
return Err(format!("+[NSCursor {selector:?}] answered nil"));
}
}
Ok(())
}
pub fn dragged_types_register_on_a_view() -> Result<(), String> {
let _pool = Pool::push();
let class = view::view_class().map_err(|error| error.to_string())?;
let bare = unsafe {
let allocated = ffi::msg(class, ffi::sel(c"alloc"));
ffi::msg(allocated, ffi::sel(c"init"))
};
let registered = unsafe {
let allocated = ffi::msg(class, ffi::sel(c"alloc"));
ffi::msg(allocated, ffi::sel(c"init"))
};
if bare.is_null() || registered.is_null() {
return Err("[[CrcblView alloc] init] was nil".into());
}
unsafe { view::register_dragged_types(registered) };
let before = unsafe { registered_types(bare) };
let after = unsafe { registered_types(registered) };
unsafe {
ffi::msg_void(bare, ffi::sel(c"release"));
ffi::msg_void(registered, ffi::sel(c"release"));
}
if !before.is_empty() {
return Err(format!(
"a CrcblView that was never registered already accepts drags of {before:?}, so \
accept_drops: false would gate nothing"
));
}
if after != [pasteboard::FILE_URL] {
return Err(format!(
"registerForDraggedTypes: left the view accepting {after:?} rather than only {:?}, \
so a file drop would not be delivered",
pasteboard::FILE_URL
));
}
Ok(())
}
unsafe fn registered_types(view: Id) -> Vec<String> {
let types = unsafe { ffi::msg(view, ffi::sel(c"registeredDraggedTypes")) };
if types.is_null() {
return Vec::new();
}
let count = unsafe { ffi::msg_usize(types, ffi::sel(c"count")) };
let mut names = Vec::with_capacity(count);
for index in 0..count {
let name = unsafe {
let send: unsafe extern "C" fn(Id, Sel, NSUInteger) -> Id = ffi::msg_send();
let string = send(types, ffi::sel(c"objectAtIndex:"), index);
ffi::string_from_nsstring(string)
};
if let Some(name) = name {
names.push(name);
}
}
names
}
pub fn pasteboard_round_trip() -> Result<(), String> {
const TEXT: &str = "crcbl M3 — pasteboard 🎮";
const RON: &[u8] = b"(kind:\"node\",id:7)";
let _pool = Pool::push();
let Some(pasteboard) = (unsafe { pasteboard::general() }) else {
return Err("+[NSPasteboard generalPasteboard] answered nil".into());
};
let before = unsafe { pasteboard::change_count(pasteboard) };
let text_type = pasteboard::pasteboard_type(MimeType::TextUtf8);
let ron_type = pasteboard::pasteboard_type(MimeType::CrcblRon);
let declared = unsafe { pasteboard::declare(pasteboard, &[text_type, ron_type]) }
.ok_or_else(|| "declareTypes:owner: could not be built".to_string())?;
if declared <= before {
return Err(format!(
"declareTypes:owner: left the change count at {declared} from {before}, so nothing \
claimed the pasteboard"
));
}
unsafe {
if !pasteboard::put(pasteboard, text_type, TEXT.as_bytes()) {
return Err(format!("setData:forType:{text_type} was refused"));
}
if !pasteboard::put(pasteboard, ron_type, RON) {
return Err(format!("setData:forType:{ron_type} was refused"));
}
}
let text = unsafe { pasteboard::get(pasteboard, text_type) };
let ron = unsafe { pasteboard::get(pasteboard, ron_type) };
let absent = unsafe { pasteboard::get(pasteboard, "sh.kryptic.crcbl.no-such-type") };
let after = unsafe { pasteboard::change_count(pasteboard) };
if text.as_deref() != Some(TEXT.as_bytes()) {
return Err(format!(
"the general pasteboard answered {:?} for {text_type} rather than what was written",
text.as_deref().map(String::from_utf8_lossy)
));
}
if ron.as_deref() != Some(RON) {
return Err(format!(
"the engine's own format did not survive the round trip: {:?}",
ron.as_deref().map(String::from_utf8_lossy)
));
}
if absent.is_some() {
return Err(format!("a type nobody wrote answered {absent:?}"));
}
if after != declared {
return Err(format!(
"the change count moved from {declared} to {after} while this process owned the \
pasteboard, so something else claimed it mid-check"
));
}
Ok(())
}
#[derive(Clone, Debug, PartialEq)]
pub struct Activation {
pub app_active: bool,
pub windows: usize,
pub title: Option<String>,
pub class: String,
pub visible: bool,
pub can_become_key: bool,
pub is_key: bool,
}
pub fn activation(title: &str) -> Result<Activation, String> {
let _pool = Pool::push();
let app = shared_application()?;
let app_active = unsafe { ffi::msg_bool(app, ffi::sel(c"isActive")) };
let windows = unsafe { windows_of(app) };
let found = windows
.iter()
.copied()
.find(|window| unsafe { title_of(*window) }.as_deref() == Some(title));
let Some(window) = found else {
return Ok(Activation {
app_active,
windows: windows.len(),
title: None,
class: "nil".to_owned(),
visible: false,
can_become_key: false,
is_key: false,
});
};
let (class, visible, can_become_key, is_key) = unsafe {
(
class_name(window),
ffi::msg_bool(window, ffi::sel(c"isVisible")),
ffi::msg_bool(window, ffi::sel(c"canBecomeKeyWindow")),
ffi::msg_bool(window, ffi::sel(c"isKeyWindow")),
)
};
Ok(Activation {
app_active,
windows: windows.len(),
title: Some(title.to_owned()),
class,
visible,
can_become_key,
is_key,
})
}
#[derive(Clone, Debug, PartialEq)]
pub struct WindowFacts {
pub app_active: bool,
pub is_key: bool,
pub title: String,
pub accepts_mouse_moved: bool,
pub first_responder: String,
pub content_view: String,
pub dragged_types: Vec<String>,
pub style_mask: usize,
pub titled: bool,
pub frame: [f64; 4],
pub content_points: [f64; 2],
pub backing_scale: f64,
pub screen_frame: Option<[f64; 4]>,
pub level: isize,
pub collection_behavior: usize,
pub hides_on_deactivate: bool,
pub opaque: bool,
pub can_become_key: bool,
pub ignores_mouse_events: bool,
}
pub fn menu_bar_screen_visible_frame() -> Result<[f64; 4], String> {
let _pool = Pool::push();
let class = ffi::class(c"NSScreen").ok_or("the runtime has no NSScreen")?;
unsafe {
let screens = ffi::msg(class.cast(), ffi::sel(c"screens"));
if screens.is_null() || ffi::msg_usize(screens, ffi::sel(c"count")) == 0 {
return Err("+[NSScreen screens] is empty".to_owned());
}
let at: unsafe extern "C" fn(Id, Sel, usize) -> Id = ffi::msg_send();
let screen = at(screens, ffi::sel(c"objectAtIndex:"), 0);
let frame = ffi::msg_rect(screen, ffi::sel(c"visibleFrame"));
Ok([
frame.origin.x,
frame.origin.y,
frame.size.width,
frame.size.height,
])
}
}
pub fn window_facts(title: &str) -> Result<WindowFacts, String> {
let _pool = Pool::push();
let app = shared_application()?;
let app_active = unsafe { ffi::msg_bool(app, ffi::sel(c"isActive")) };
let window = unsafe { window_titled(app, title) }?;
Ok(unsafe { facts_of(window, app_active) })
}
pub fn key_window() -> Result<WindowFacts, String> {
let _pool = Pool::push();
let app = shared_application()?;
let (app_active, window) = unsafe {
(
ffi::msg_bool(app, ffi::sel(c"isActive")),
ffi::msg(app, ffi::sel(c"keyWindow")),
)
};
if window.is_null() {
return Err(format!(
"-[NSApp keyWindow] is nil and the application is {}active, so no window of this \
process has the keyboard; injected input would go to whatever does",
if app_active { "" } else { "in" }
));
}
Ok(unsafe { facts_of(window, app_active) })
}
pub fn resize_window(title: &str, width: f64, height: f64) -> Result<(), String> {
let _pool = Pool::push();
let app = shared_application()?;
let window = unsafe { window_titled(app, title) }?;
unsafe {
ffi::msg_set_size(
window,
ffi::sel(c"setContentSize:"),
NSSize { width, height },
);
}
Ok(())
}
fn shared_application() -> Result<Id, String> {
let Some(class) = ffi::class(c"NSApplication") else {
return Err("the Objective-C runtime has no NSApplication".into());
};
let app = unsafe { ffi::msg(class, ffi::sel(c"sharedApplication")) };
if app.is_null() {
return Err("[NSApplication sharedApplication] answered nil".into());
}
Ok(app)
}
unsafe fn windows_of(app: Id) -> Vec<Id> {
let windows = unsafe { ffi::msg(app, ffi::sel(c"windows")) };
if windows.is_null() {
return Vec::new();
}
let count = unsafe { ffi::msg_usize(windows, ffi::sel(c"count")) };
(0..count)
.map(|index| {
unsafe {
let send: unsafe extern "C" fn(Id, Sel, NSUInteger) -> Id = ffi::msg_send();
send(windows, ffi::sel(c"objectAtIndex:"), index)
}
})
.collect()
}
unsafe fn window_titled(app: Id, title: &str) -> Result<Id, String> {
let windows = unsafe { windows_of(app) };
let titles: Vec<Option<String>> = windows
.iter()
.map(|window| unsafe { title_of(*window) })
.collect();
windows
.iter()
.zip(&titles)
.find(|(_, seen)| seen.as_deref() == Some(title))
.map(|(window, _)| *window)
.ok_or_else(|| {
format!(
"no window of this process is titled {title:?}; -[NSApp windows] holds {} and \
they are titled {titles:?}",
windows.len()
)
})
}
unsafe fn title_of(window: Id) -> Option<String> {
unsafe { ffi::string_from_nsstring(ffi::msg(window, ffi::sel(c"title"))) }
}
unsafe fn facts_of(window: Id, app_active: bool) -> WindowFacts {
let (level, collection_behavior, hides_on_deactivate, opaque, can_become_key) = unsafe {
(
ffi::msg_usize(window, ffi::sel(c"level")) as isize,
ffi::msg_usize(window, ffi::sel(c"collectionBehavior")),
ffi::msg_bool(window, ffi::sel(c"hidesOnDeactivate")),
ffi::msg_bool(window, ffi::sel(c"isOpaque")),
ffi::msg_bool(window, ffi::sel(c"canBecomeKeyWindow")),
)
};
let ignores_mouse_events = unsafe { ffi::msg_bool(window, ffi::sel(c"ignoresMouseEvents")) };
let (is_key, accepts_mouse_moved, responder, content, style_mask, frame, backing_scale, screen) = unsafe {
(
ffi::msg_bool(window, ffi::sel(c"isKeyWindow")),
ffi::msg_bool(window, ffi::sel(c"acceptsMouseMovedEvents")),
ffi::msg(window, ffi::sel(c"firstResponder")),
ffi::msg(window, ffi::sel(c"contentView")),
ffi::msg_usize(window, ffi::sel(c"styleMask")),
ffi::msg_rect(window, ffi::sel(c"frame")),
ffi::msg_f64(window, ffi::sel(c"backingScaleFactor")),
ffi::msg(window, ffi::sel(c"screen")),
)
};
let title = unsafe { title_of(window) }.unwrap_or_default();
let content_frame = unsafe { ffi::msg_rect(content, ffi::sel(c"frame")) };
let dragged_types = unsafe { registered_types(content) };
let screen_frame = (!screen.is_null()).then(|| {
let frame = unsafe { ffi::msg_rect(screen, ffi::sel(c"frame")) };
[
frame.origin.x,
frame.origin.y,
frame.size.width,
frame.size.height,
]
});
WindowFacts {
app_active,
is_key,
title,
accepts_mouse_moved,
first_responder: unsafe { class_name(responder) },
content_view: unsafe { class_name(content) },
dragged_types,
style_mask,
titled: style_mask & style::TITLED != 0,
frame: [
frame.origin.x,
frame.origin.y,
frame.size.width,
frame.size.height,
],
content_points: [content_frame.size.width, content_frame.size.height],
backing_scale,
screen_frame,
level,
collection_behavior,
hides_on_deactivate,
opaque,
can_become_key,
ignores_mouse_events,
}
}
unsafe fn class_name(object: Id) -> String {
if object.is_null() {
return "nil".to_owned();
}
let name = unsafe { object_getClassName(object) };
if name.is_null() {
return "?".to_owned();
}
unsafe { CStr::from_ptr(name) }
.to_string_lossy()
.into_owned()
}