use core::ffi::CStr;
use core::ptr;
use std::path::PathBuf;
use std::sync::OnceLock;
use crcbl_core::input::ButtonState;
use super::app;
use super::events::RawEvent;
use super::ffi::{self, Class, Id, Imp, NSPoint, NSRange, NSRect, NSUInteger, ObjcBool, Sel, YES};
use super::keys;
use super::pasteboard;
const VIEW_CLASS: &CStr = c"CrcblView";
static VIEW: OnceLock<Result<usize, String>> = OnceLock::new();
unsafe fn seconds(event: Id) -> f64 {
unsafe { ffi::msg_f64(event, ffi::sel(c"timestamp")) }
}
unsafe fn flags(event: Id) -> NSUInteger {
unsafe { ffi::msg_usize(event, ffi::sel(c"modifierFlags")) }
}
unsafe fn keycode(event: Id) -> u16 {
let send: unsafe extern "C" fn(Id, Sel) -> u16 = unsafe { ffi::msg_send() };
unsafe { send(event, ffi::sel(c"keyCode")) }
}
unsafe fn location(event: Id) -> NSPoint {
let send: unsafe extern "C" fn(Id, Sel) -> NSPoint = unsafe { ffi::msg_send() };
unsafe { send(event, ffi::sel(c"locationInWindow")) }
}
unsafe fn first_character(event: Id) -> Option<char> {
let string = unsafe { ffi::msg(event, ffi::sel(c"charactersIgnoringModifiers")) };
if string.is_null() {
return None;
}
if unsafe { ffi::msg_usize(string, ffi::sel(c"length")) } == 0 {
return None;
}
let unit = unsafe {
let send: unsafe extern "C" fn(Id, Sel, NSUInteger) -> u16 = ffi::msg_send();
send(string, ffi::sel(c"characterAtIndex:"), 0)
};
char::from_u32(u32::from(unit))
}
unsafe fn plain_string(string: Id) -> Option<String> {
if string.is_null() {
return None;
}
let plain = match ffi::class(c"NSAttributedString") {
Some(attributed) if unsafe { is_kind_of(string, attributed) } => {
unsafe { ffi::msg(string, ffi::sel(c"string")) }
}
_ => string,
};
unsafe { ffi::string_from_nsstring(plain) }
}
unsafe fn is_kind_of(object: Id, class: Class) -> bool {
let send: unsafe extern "C" fn(Id, Sel, Class) -> ObjcBool = unsafe { ffi::msg_send() };
unsafe { send(object, ffi::sel(c"isKindOfClass:"), class) != ffi::NO }
}
unsafe fn window_of(view: Id) -> Id {
unsafe { ffi::msg(view, ffi::sel(c"window")) }
}
unsafe fn delegate_of(view: Id) -> Id {
let window = unsafe { window_of(view) };
if window.is_null() {
return ptr::null_mut();
}
unsafe { ffi::msg(window, ffi::sel(c"delegate")) }
}
unsafe fn record(view: Id, make: impl FnOnce(usize) -> RawEvent) {
let window = unsafe { window_of(view) };
if window.is_null() {
return;
}
let delegate = unsafe { delegate_of(view) };
app::record(delegate, make(window as usize));
}
unsafe extern "C" fn accepts_first_responder(_view: Id, _cmd: Sel) -> ObjcBool {
YES
}
unsafe extern "C" fn accepts_first_mouse(_view: Id, _cmd: Sel, _event: Id) -> ObjcBool {
YES
}
unsafe extern "C" fn key_down(view: Id, _cmd: Sel, event: Id) {
unsafe {
let stamp = seconds(event);
record(view, |window| RawEvent::Key {
window,
keycode: keycode(event),
character: first_character(event),
flags: flags(event),
state: ButtonState::Pressed,
repeat: ffi::msg_bool(event, ffi::sel(c"isARepeat")),
seconds: stamp,
});
app::with_shared(delegate_of(view), |shared| shared.set_keying(stamp));
interpret(view, event);
}
}
unsafe fn interpret(view: Id, event: Id) {
let Some(array_class) = ffi::class(c"NSArray") else {
return;
};
unsafe {
let array = ffi::msg1(array_class, ffi::sel(c"arrayWithObject:"), event);
ffi::msg1_void(view, ffi::sel(c"interpretKeyEvents:"), array);
}
}
unsafe extern "C" fn key_up(view: Id, _cmd: Sel, event: Id) {
unsafe {
record(view, |window| RawEvent::Key {
window,
keycode: keycode(event),
character: first_character(event),
flags: flags(event),
state: ButtonState::Released,
repeat: false,
seconds: seconds(event),
});
}
}
unsafe extern "C" fn flags_changed(view: Id, _cmd: Sel, event: Id) {
unsafe {
record(view, |window| RawEvent::FlagsChanged {
window,
keycode: keycode(event),
flags: flags(event),
seconds: seconds(event),
});
}
}
unsafe extern "C" fn mouse_moved(view: Id, _cmd: Sel, event: Id) {
unsafe {
record(view, |window| RawEvent::PointerMotion {
window,
location: location(event),
dx: ffi::msg_f64(event, ffi::sel(c"deltaX")),
dy: ffi::msg_f64(event, ffi::sel(c"deltaY")),
seconds: seconds(event),
});
}
}
unsafe extern "C" fn scroll_wheel(view: Id, _cmd: Sel, event: Id) {
unsafe {
record(view, |window| RawEvent::Scroll {
window,
dx: ffi::msg_f64(event, ffi::sel(c"scrollingDeltaX")),
dy: ffi::msg_f64(event, ffi::sel(c"scrollingDeltaY")),
precise: ffi::msg_bool(event, ffi::sel(c"hasPreciseScrollingDeltas")),
location: location(event),
flags: flags(event),
seconds: seconds(event),
});
}
}
unsafe fn button(view: Id, event: Id, state: ButtonState) {
unsafe {
let send: unsafe extern "C" fn(Id, Sel) -> isize = ffi::msg_send();
let number = send(event, ffi::sel(c"buttonNumber"));
record(view, |window| RawEvent::Button {
window,
number,
state,
location: location(event),
flags: flags(event),
seconds: seconds(event),
});
}
}
unsafe extern "C" fn mouse_down(view: Id, _cmd: Sel, event: Id) {
unsafe { button(view, event, ButtonState::Pressed) };
}
unsafe extern "C" fn mouse_up(view: Id, _cmd: Sel, event: Id) {
unsafe { button(view, event, ButtonState::Released) };
}
unsafe extern "C" fn mouse_entered(view: Id, _cmd: Sel, event: Id) {
unsafe {
record(view, |window| RawEvent::PointerFocus {
window,
entered: true,
location: location(event),
seconds: seconds(event),
});
}
}
unsafe extern "C" fn mouse_exited(view: Id, _cmd: Sel, event: Id) {
unsafe {
record(view, |window| RawEvent::PointerFocus {
window,
entered: false,
location: location(event),
seconds: seconds(event),
});
}
}
unsafe extern "C" fn cursor_update(view: Id, _cmd: Sel, _event: Id) {
let window = unsafe { window_of(view) };
if window.is_null() {
return;
}
let wanted =
unsafe { app::with_shared(delegate_of(view), |s| s.cursor_of(window as usize)) }.flatten();
unsafe {
match wanted {
Some(cursor) => ffi::msg_void(cursor as Id, ffi::sel(c"set")),
None => {
if let Some(class) = ffi::class(c"NSCursor") {
let arrow = ffi::msg(class, ffi::sel(c"arrowCursor"));
ffi::msg_void(arrow, ffi::sel(c"set"));
}
}
}
}
}
unsafe fn dragging_location(sender: Id) -> NSPoint {
let send: unsafe extern "C" fn(Id, Sel) -> NSPoint = unsafe { ffi::msg_send() };
unsafe { send(sender, ffi::sel(c"draggingLocation")) }
}
unsafe extern "C" fn dragging_entered(_view: Id, _cmd: Sel, _sender: Id) -> NSUInteger {
ffi::value::DRAG_OPERATION_COPY
}
unsafe extern "C" fn prepare_for_drag_operation(_view: Id, _cmd: Sel, _sender: Id) -> ObjcBool {
YES
}
unsafe extern "C" fn perform_drag_operation(view: Id, _cmd: Sel, sender: Id) -> ObjcBool {
let pasteboard = unsafe { ffi::msg(sender, ffi::sel(c"draggingPasteboard")) };
if pasteboard.is_null() {
return ffi::NO;
}
let paths: Vec<PathBuf> = unsafe { pasteboard::file_urls(pasteboard) }
.iter()
.filter_map(|url| pasteboard::path_from_file_url(url))
.collect();
if paths.is_empty() {
return ffi::NO;
}
let window = unsafe { window_of(view) };
if window.is_null() {
return ffi::NO;
}
let delegate = unsafe { delegate_of(view) };
if app::with_shared(delegate, |shared| shared.push_drop(window as usize, paths)).is_none() {
return ffi::NO;
}
let seconds = unsafe { ffi::CACurrentMediaTime() };
app::record(
delegate,
RawEvent::FilesDropped {
window: window as usize,
location: unsafe { dragging_location(sender) },
seconds,
},
);
YES
}
unsafe extern "C" fn insert_text(view: Id, _cmd: Sel, string: Id, _replacement: NSRange) {
let Some(text) = (unsafe { plain_string(string) }) else {
return;
};
let text: String = text.chars().filter(|c| keys::is_text(*c)).collect();
if text.is_empty() {
return;
}
let window = unsafe { window_of(view) };
if window.is_null() {
return;
}
let delegate = unsafe { delegate_of(view) };
let stamp = app::with_shared(delegate, |shared| {
shared.push_text(window as usize, text);
shared.keying()
});
let Some(seconds) = stamp else { return };
app::record(
delegate,
RawEvent::TextCommitted {
window: window as usize,
seconds,
},
);
}
unsafe extern "C" fn do_command_by_selector(_view: Id, _cmd: Sel, _selector: Sel) {}
unsafe extern "C" fn set_marked_text(
view: Id,
_cmd: Sel,
string: Id,
_selected: NSRange,
_replacement: NSRange,
) {
let units = unsafe { plain_string(string) }.map_or(0, |text| text.encode_utf16().count());
unsafe { app::with_shared(delegate_of(view), |shared| shared.set_marked(units)) };
}
unsafe extern "C" fn unmark_text(view: Id, _cmd: Sel) {
unsafe { app::with_shared(delegate_of(view), |shared| shared.set_marked(0)) };
}
unsafe extern "C" fn has_marked_text(view: Id, _cmd: Sel) -> ObjcBool {
let marked = unsafe { app::with_shared(delegate_of(view), super::app::Shared::marked) };
if marked.unwrap_or(0) > 0 {
YES
} else {
ffi::NO
}
}
unsafe extern "C" fn marked_range(view: Id, _cmd: Sel) -> NSRange {
let marked = unsafe { app::with_shared(delegate_of(view), super::app::Shared::marked) };
match marked.unwrap_or(0) {
0 => NSRange::NONE,
units => NSRange::new(0, units),
}
}
unsafe extern "C" fn selected_range(_view: Id, _cmd: Sel) -> NSRange {
NSRange::NONE
}
unsafe extern "C" fn attributed_substring(
_view: Id,
_cmd: Sel,
_range: NSRange,
_actual: *mut NSRange,
) -> Id {
ptr::null_mut()
}
unsafe extern "C" fn valid_attributes(_view: Id, _cmd: Sel) -> Id {
let Some(class) = ffi::class(c"NSArray") else {
return ptr::null_mut();
};
unsafe { ffi::msg(class, ffi::sel(c"array")) }
}
unsafe extern "C" fn first_rect(
view: Id,
_cmd: Sel,
_range: NSRange,
_actual: *mut NSRange,
) -> NSRect {
let window = unsafe { window_of(view) };
if window.is_null() {
return NSRect::default();
}
unsafe {
let convert: unsafe extern "C" fn(Id, Sel, NSRect) -> NSRect = ffi::msg_send_stret();
convert(
window,
ffi::sel(c"convertRectToScreen:"),
NSRect::new(0.0, 0.0, 0.0, 0.0),
)
}
}
unsafe extern "C" fn character_index_for_point(
_view: Id,
_cmd: Sel,
_point: NSPoint,
) -> NSUInteger {
0
}
fn event_imp(implementation: unsafe extern "C" fn(Id, Sel, Id)) -> Imp {
unsafe { core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(implementation) }
}
fn bool_imp(implementation: unsafe extern "C" fn(Id, Sel) -> ObjcBool) -> Imp {
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel) -> ObjcBool, Imp>(implementation)
}
}
fn bool_event_imp(implementation: unsafe extern "C" fn(Id, Sel, Id) -> ObjcBool) -> Imp {
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id) -> ObjcBool, Imp>(implementation)
}
}
fn drag_imp(implementation: unsafe extern "C" fn(Id, Sel, Id) -> NSUInteger) -> Imp {
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id) -> NSUInteger, Imp>(implementation)
}
}
fn range_imp(implementation: unsafe extern "C" fn(Id, Sel) -> NSRange) -> Imp {
unsafe { core::mem::transmute::<unsafe extern "C" fn(Id, Sel) -> NSRange, Imp>(implementation) }
}
pub(super) fn view_class() -> Result<Class, crate::ShellError> {
let outcome = VIEW.get_or_init(|| {
let Some(superclass) = ffi::class(c"NSView") else {
return Err(
"the Objective-C runtime has no NSView, so this image has no AppKit in it"
.to_string(),
);
};
let class = unsafe { ffi::objc_allocateClassPair(superclass, VIEW_CLASS.as_ptr(), 0) };
if class.is_null() {
let Some(existing) = ffi::class(VIEW_CLASS) else {
return Err(
"objc_allocateClassPair refused CrcblView and no class of that name exists"
.to_string(),
);
};
crcbl_core::log::debug!(
"the {VIEW_CLASS:?} class was already registered in this process"
);
return Ok(existing as usize);
}
install(class)?;
unsafe { ffi::objc_registerClassPair(class) };
Ok(class as usize)
});
match outcome {
Ok(class) => Ok(*class as Class),
Err(detail) => Err(crate::ShellError::Connect {
backend: crate::ShellBackend::AppKit,
detail: detail.clone(),
}),
}
}
fn install(class: Class) -> Result<(), String> {
let void_event = c"v@:@";
let bool_nothing = std::ffi::CString::new(format!("{}@:", ffi::ENC_BOOL))
.expect("the encoding characters contain no NUL");
let bool_event = std::ffi::CString::new(format!("{}@:@", ffi::ENC_BOOL))
.expect("the encoding characters contain no NUL");
let range_nothing = std::ffi::CString::new(format!("{}@:", ffi::ENC_RANGE))
.expect("the encoding characters contain no NUL");
let insert = std::ffi::CString::new(format!("v@:@{}", ffi::ENC_RANGE))
.expect("the encoding characters contain no NUL");
let set_marked = std::ffi::CString::new(format!("v@:@{0}{0}", ffi::ENC_RANGE))
.expect("the encoding characters contain no NUL");
let substring = std::ffi::CString::new(format!("@@:{0}^{0}", ffi::ENC_RANGE))
.expect("the encoding characters contain no NUL");
let first_rect_enc =
std::ffi::CString::new(format!("{}@:{1}^{1}", ffi::ENC_RECT, ffi::ENC_RANGE))
.expect("the encoding characters contain no NUL");
let index_for_point = std::ffi::CString::new(format!("Q@:{}", ffi::ENC_POINT))
.expect("the encoding characters contain no NUL");
let drag_event = c"Q@:@";
let methods: [(&CStr, Imp, &CStr); 26] = [
(
c"acceptsFirstResponder",
bool_imp(accepts_first_responder),
bool_nothing.as_c_str(),
),
(
c"acceptsFirstMouse:",
bool_event_imp(accepts_first_mouse),
bool_event.as_c_str(),
),
(c"keyDown:", event_imp(key_down), void_event),
(c"keyUp:", event_imp(key_up), void_event),
(c"flagsChanged:", event_imp(flags_changed), void_event),
(c"mouseMoved:", event_imp(mouse_moved), void_event),
(c"mouseDragged:", event_imp(mouse_moved), void_event),
(c"rightMouseDragged:", event_imp(mouse_moved), void_event),
(c"otherMouseDragged:", event_imp(mouse_moved), void_event),
(c"mouseDown:", event_imp(mouse_down), void_event),
(c"mouseUp:", event_imp(mouse_up), void_event),
(c"rightMouseDown:", event_imp(mouse_down), void_event),
(c"rightMouseUp:", event_imp(mouse_up), void_event),
(c"otherMouseDown:", event_imp(mouse_down), void_event),
(c"otherMouseUp:", event_imp(mouse_up), void_event),
(c"scrollWheel:", event_imp(scroll_wheel), void_event),
(c"mouseEntered:", event_imp(mouse_entered), void_event),
(c"mouseExited:", event_imp(mouse_exited), void_event),
(c"cursorUpdate:", event_imp(cursor_update), void_event),
(c"draggingEntered:", drag_imp(dragging_entered), drag_event),
(c"draggingUpdated:", drag_imp(dragging_entered), drag_event),
(
c"prepareForDragOperation:",
bool_event_imp(prepare_for_drag_operation),
bool_event.as_c_str(),
),
(
c"performDragOperation:",
bool_event_imp(perform_drag_operation),
bool_event.as_c_str(),
),
(
c"insertText:replacementRange:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id, NSRange), Imp>(insert_text)
},
insert.as_c_str(),
),
(
c"doCommandBySelector:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Sel), Imp>(
do_command_by_selector,
)
},
c"v@::",
),
(
c"setMarkedText:selectedRange:replacementRange:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id, NSRange, NSRange), Imp>(
set_marked_text,
)
},
set_marked.as_c_str(),
),
];
for (name, imp, types) in methods {
add(class, name, imp, types)?;
}
add(
class,
c"unmarkText",
unsafe { core::mem::transmute::<unsafe extern "C" fn(Id, Sel), Imp>(unmark_text) },
c"v@:",
)?;
add(
class,
c"hasMarkedText",
bool_imp(has_marked_text),
bool_nothing.as_c_str(),
)?;
add(
class,
c"markedRange",
range_imp(marked_range),
range_nothing.as_c_str(),
)?;
add(
class,
c"selectedRange",
range_imp(selected_range),
range_nothing.as_c_str(),
)?;
add(
class,
c"attributedSubstringForProposedRange:actualRange:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, NSRange, *mut NSRange) -> Id, Imp>(
attributed_substring,
)
},
substring.as_c_str(),
)?;
add(
class,
c"validAttributesForMarkedText",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel) -> Id, Imp>(valid_attributes)
},
c"@@:",
)?;
add(
class,
c"firstRectForCharacterRange:actualRange:",
unsafe {
core::mem::transmute::<
unsafe extern "C" fn(Id, Sel, NSRange, *mut NSRange) -> NSRect,
Imp,
>(first_rect)
},
first_rect_enc.as_c_str(),
)?;
add(
class,
c"characterIndexForPoint:",
unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, NSPoint) -> NSUInteger, Imp>(
character_index_for_point,
)
},
index_for_point.as_c_str(),
)?;
let protocol = unsafe { ffi::objc_getProtocol(c"NSTextInputClient".as_ptr()) };
if protocol.is_null() {
return Err(
"the Objective-C runtime has no NSTextInputClient protocol, so no input method \
could reach this view and text input would be silently dead"
.to_string(),
);
}
if unsafe { ffi::class_addProtocol(class, protocol) } == ffi::NO {
return Err("class_addProtocol refused NSTextInputClient on CrcblView".to_string());
}
Ok(())
}
fn add(class: Class, name: &CStr, imp: Imp, types: &CStr) -> Result<(), String> {
if unsafe { ffi::class_addMethod(class, ffi::sel(name), imp, types.as_ptr()) } == ffi::NO {
return Err(format!("class_addMethod refused {name:?} on CrcblView"));
}
Ok(())
}
pub(super) unsafe fn add_tracking_area(view: Id) {
let Some(class) = ffi::class(c"NSTrackingArea") else {
crcbl_core::log::warn!(
"the Objective-C runtime has no NSTrackingArea; pointer enter and leave will not \
be reported for this window"
);
return;
};
unsafe {
let allocated = ffi::msg(class, ffi::sel(c"alloc"));
let init: unsafe extern "C" fn(Id, Sel, NSRect, NSUInteger, Id, Id) -> Id = ffi::msg_send();
let area = init(
allocated,
ffi::sel(c"initWithRect:options:owner:userInfo:"),
NSRect::new(0.0, 0.0, 0.0, 0.0),
ffi::tracking::OPTIONS,
view,
ptr::null_mut(),
);
if area.is_null() {
crcbl_core::log::warn!(
"-[NSTrackingArea initWithRect:options:owner:userInfo:] returned nil"
);
return;
}
ffi::msg1_void(view, ffi::sel(c"addTrackingArea:"), area);
ffi::msg_void(area, ffi::sel(c"release"));
}
}
pub(super) unsafe fn register_dragged_types(view: Id) {
let Some(class) = ffi::class(c"NSArray") else {
crcbl_core::log::warn!(
"the Objective-C runtime has no NSArray; this window cannot register for file \
drops and will receive none"
);
return;
};
let Some(kind) = (unsafe { ffi::nsstring(pasteboard::FILE_URL) }) else {
crcbl_core::log::warn!("the dragged-type identifier could not be made into an NSString");
return;
};
unsafe {
let types = ffi::msg1(class, ffi::sel(c"arrayWithObject:"), kind);
ffi::msg1_void(view, ffi::sel(c"registerForDraggedTypes:"), types);
}
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
fn the_view_class_installs_every_method_input_arrives_through() {
let class = view_class().expect("CrcblView is built from NSObject-level runtime calls");
for selector in [
c"acceptsFirstResponder",
c"acceptsFirstMouse:",
c"keyDown:",
c"keyUp:",
c"flagsChanged:",
c"mouseMoved:",
c"mouseDragged:",
c"rightMouseDragged:",
c"otherMouseDragged:",
c"mouseDown:",
c"mouseUp:",
c"rightMouseDown:",
c"rightMouseUp:",
c"otherMouseDown:",
c"otherMouseUp:",
c"scrollWheel:",
c"mouseEntered:",
c"mouseExited:",
c"cursorUpdate:",
c"draggingEntered:",
c"draggingUpdated:",
c"prepareForDragOperation:",
c"performDragOperation:",
] {
let installed = unsafe {
let send: unsafe extern "C" fn(Id, Sel, Sel) -> ObjcBool = ffi::msg_send();
send(
class,
ffi::sel(c"instancesRespondToSelector:"),
ffi::sel(selector),
) != ffi::NO
};
assert!(installed, "CrcblView does not implement {selector:?}");
}
}
#[test]
fn the_view_class_is_a_text_input_client_and_answers_every_required_method() {
let class = view_class().expect("CrcblView is built");
let protocol = unsafe { ffi::objc_getProtocol(c"NSTextInputClient".as_ptr()) };
assert!(!protocol.is_null(), "this AppKit has no NSTextInputClient");
let conforms = unsafe {
let send: unsafe extern "C" fn(Id, Sel, Id) -> ObjcBool = ffi::msg_send();
send(class, ffi::sel(c"conformsToProtocol:"), protocol) != ffi::NO
};
assert!(conforms, "CrcblView does not claim NSTextInputClient");
for selector in [
c"insertText:replacementRange:",
c"doCommandBySelector:",
c"setMarkedText:selectedRange:replacementRange:",
c"unmarkText",
c"selectedRange",
c"markedRange",
c"hasMarkedText",
c"attributedSubstringForProposedRange:actualRange:",
c"validAttributesForMarkedText",
c"firstRectForCharacterRange:actualRange:",
c"characterIndexForPoint:",
] {
let installed = unsafe {
let send: unsafe extern "C" fn(Id, Sel, Sel) -> ObjcBool = ffi::msg_send();
send(
class,
ffi::sel(c"instancesRespondToSelector:"),
ffi::sel(selector),
) != ffi::NO
};
assert!(
installed,
"NSTextInputClient requires {selector:?} and CrcblView does not have it"
);
}
}
#[test]
fn the_class_is_built_once_and_answered_again_afterwards() {
let first = view_class().expect("built");
let second = view_class().expect("built again");
assert_eq!(first, second);
assert_eq!(
ffi::class(VIEW_CLASS).expect("registered"),
first,
"the registered class is the one this module hands out"
);
}
}