use core::ffi::CStr;
use core::ptr;
use std::sync::OnceLock;
use crate::{
DisplayMode, MonitorId, PhysicalSize, ShellBackend, ShellError, SizeConstraints, WindowDesc,
};
use super::ffi::{self, Class, Id, Imp, NSRect, NSSize, ObjcBool, Sel, YES};
use super::geometry;
use super::monitors::Screen;
use super::panel::Panel;
use super::shell::{AppKitShell, AppWindow};
use super::view;
use crate::{PanelKeyboard, PopupInput};
const WINDOW_CLASS: &CStr = c"CrcblWindow";
static WINDOW: OnceLock<Result<usize, String>> = OnceLock::new();
static PANEL: OnceLock<Result<usize, String>> = OnceLock::new();
static PASSIVE_PANEL: OnceLock<Result<usize, String>> = OnceLock::new();
unsafe extern "C" fn can_become(_window: Id, _cmd: Sel) -> ObjcBool {
YES
}
unsafe extern "C" fn cannot_become(_window: Id, _cmd: Sel) -> ObjcBool {
ffi::NO
}
unsafe extern "C" fn constrain_frame(_window: Id, _cmd: Sel, frame: NSRect, _screen: Id) -> NSRect {
frame
}
fn window_class() -> Result<Class, ShellError> {
build_class(&ClassSpec {
superclass: c"NSWindow",
name: WINDOW_CLASS,
key: can_become,
main: can_become,
cell: &WINDOW,
})
}
pub(super) fn panel_class(takes_keyboard: bool) -> Result<Class, ShellError> {
if takes_keyboard {
build_class(&ClassSpec {
superclass: c"NSPanel",
name: c"CrcblPanel",
key: can_become,
main: cannot_become,
cell: &PANEL,
})
} else {
build_class(&ClassSpec {
superclass: c"NSPanel",
name: c"CrcblPassivePanel",
key: cannot_become,
main: cannot_become,
cell: &PASSIVE_PANEL,
})
}
}
struct ClassSpec {
superclass: &'static CStr,
name: &'static CStr,
key: unsafe extern "C" fn(Id, Sel) -> ObjcBool,
main: unsafe extern "C" fn(Id, Sel) -> ObjcBool,
cell: &'static OnceLock<Result<usize, String>>,
}
fn build_class(spec: &ClassSpec) -> Result<Class, ShellError> {
let outcome = spec.cell.get_or_init(|| {
let Some(superclass) = ffi::class(spec.superclass) else {
return Err(format!(
"the Objective-C runtime has no {:?}, so this image has no AppKit in it",
spec.superclass
));
};
let class = unsafe { ffi::objc_allocateClassPair(superclass, spec.name.as_ptr(), 0) };
if class.is_null() {
let Some(existing) = ffi::class(spec.name) else {
return Err(format!(
"objc_allocateClassPair refused {:?} and no class of that name exists",
spec.name
));
};
crcbl_core::log::debug!(
"the {:?} class was already registered in this process",
spec.name
);
return Ok(existing as usize);
}
let predicate = std::ffi::CString::new(format!("{}@:", ffi::ENC_BOOL))
.expect("the encoding characters contain no NUL");
for (name, answer) in [
(c"canBecomeKeyWindow", spec.key),
(c"canBecomeMainWindow", spec.main),
] {
let imp = unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel) -> ObjcBool, Imp>(answer)
};
if unsafe { ffi::class_addMethod(class, ffi::sel(name), imp, predicate.as_ptr()) }
== ffi::NO
{
return Err(format!(
"class_addMethod refused {name:?} on {:?}",
spec.name
));
}
}
let constrain = std::ffi::CString::new(format!("{rect}@:{rect}@", rect = ffi::ENC_RECT))
.expect("the encoding characters contain no NUL");
let imp = unsafe {
core::mem::transmute::<unsafe extern "C" fn(Id, Sel, NSRect, Id) -> NSRect, Imp>(
constrain_frame,
)
};
if unsafe {
ffi::class_addMethod(
class,
ffi::sel(c"constrainFrameRect:toScreen:"),
imp,
constrain.as_ptr(),
)
} == ffi::NO
{
return Err(format!(
"class_addMethod refused constrainFrameRect:toScreen: on {:?}",
spec.name
));
}
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(),
}),
}
}
unsafe fn make_transparent(window: Id, layer: Id) {
let Some(color) = ffi::class(c"NSColor") else {
return;
};
unsafe {
ffi::msg_set_bool(window, ffi::sel(c"setOpaque:"), false);
let clear = ffi::msg(color.cast(), ffi::sel(c"clearColor"));
ffi::msg1_void(window, ffi::sel(c"setBackgroundColor:"), clear);
ffi::msg_set_bool(window, ffi::sel(c"setHasShadow:"), false);
ffi::msg_set_bool(layer, ffi::sel(c"setOpaque:"), false);
}
}
#[derive(Clone, Copy, Debug)]
pub(super) enum Kind<'a> {
Window,
Panel(&'a Panel),
Popup { frame: NSRect, input: PopupInput },
}
#[derive(Clone, Copy, Debug)]
pub(super) struct Created {
pub window: Id,
pub view: Id,
pub layer: Id,
pub scale: f64,
pub size: PhysicalSize,
}
pub(super) unsafe fn backing_size(view: Id) -> PhysicalSize {
let backing = unsafe {
let bounds = ffi::msg_rect(view, ffi::sel(c"bounds"));
let convert: unsafe extern "C" fn(Id, Sel, NSRect) -> NSRect = ffi::msg_send_stret();
convert(view, ffi::sel(c"convertRectToBacking:"), bounds)
};
geometry::physical_size(backing.size, 1.0)
}
pub(super) unsafe fn backing_scale(window: Id) -> f64 {
geometry::usable_scale(unsafe { ffi::msg_f64(window, ffi::sel(c"backingScaleFactor")) })
}
fn frame_for_content(mask: usize, content: NSRect) -> NSRect {
let Some(class) = ffi::class(c"NSWindow") else {
return content;
};
let send: unsafe extern "C" fn(Id, Sel, NSRect, usize) -> NSRect =
unsafe { ffi::msg_send_stret() };
unsafe {
send(
class,
ffi::sel(c"frameRectForContentRect:styleMask:"),
content,
mask,
)
}
}
pub(super) unsafe fn size_layer(layer: Id, view: Id, scale: f64) {
unsafe {
ffi::msg_send::<unsafe extern "C" fn(Id, Sel, f64)>()(
layer,
ffi::sel(c"setContentsScale:"),
scale,
);
let size = backing_size(view);
ffi::msg_set_size(
layer,
ffi::sel(c"setDrawableSize:"),
NSSize::new(f64::from(size.width), f64::from(size.height)),
);
}
}
pub(super) unsafe fn apply_constraints(window: Id, constraints: SizeConstraints) {
let (min, max) = geometry::content_limits(constraints);
unsafe {
ffi::msg_set_size(window, ffi::sel(c"setContentMinSize:"), min);
ffi::msg_set_size(window, ffi::sel(c"setContentMaxSize:"), max);
ffi::msg_set_size(
window,
ffi::sel(c"setContentAspectRatio:"),
geometry::aspect_size(constraints.aspect),
);
}
}
impl AppKitShell {
pub(super) fn create_native_window(
&self,
desc: &WindowDesc<'_>,
target: Option<&Screen>,
kind: Kind<'_>,
) -> Result<Created, ShellError> {
let panel = match kind {
Kind::Panel(panel) => Some(panel),
Kind::Window | Kind::Popup { .. } => None,
};
let class = match kind {
Kind::Panel(panel) => panel_class(panel.keyboard != PanelKeyboard::None)?,
Kind::Popup { input, .. } => panel_class(input == PopupInput::Grab)?,
Kind::Window => window_class()?,
};
let view_class = view::view_class()?;
let Some(layer_class) = ffi::class(c"CAMetalLayer") else {
return Err(ShellError::Connect {
backend: ShellBackend::AppKit,
detail: "the Objective-C runtime has no CAMetalLayer, so this image has no \
QuartzCore in it"
.to_string(),
});
};
let mask = match kind {
Kind::Panel(_) | Kind::Popup { .. } => geometry::PANEL_STYLE_MASK,
Kind::Window => geometry::style_mask(desc.mode, desc.resizable),
};
let content = match (kind, desc.mode, target) {
(Kind::Popup { frame, .. }, _, _) => frame,
(Kind::Panel(panel), _, Some(screen)) => panel.frame_on(screen),
(Kind::Window, DisplayMode::Borderless { .. }, Some(screen)) => screen.frame,
(_, _, Some(screen)) => geometry::centred(screen.visible, geometry::points(desc.size)),
(_, _, None) => NSRect::new(0.0, 0.0, desc.size.width, desc.size.height),
};
let window = unsafe {
let allocated = ffi::msg(class, ffi::sel(c"alloc"));
let init: unsafe extern "C" fn(Id, Sel, NSRect, usize, usize, ObjcBool) -> Id =
ffi::msg_send();
init(
allocated,
ffi::sel(c"initWithContentRect:styleMask:backing:defer:"),
content,
mask,
ffi::value::BACKING_BUFFERED,
ffi::NO,
)
};
if window.is_null() {
return Err(ShellError::WindowCreation(
"-[NSWindow initWithContentRect:styleMask:backing:defer:] returned nil".to_string(),
));
}
unsafe { ffi::msg_set_bool(window, ffi::sel(c"setReleasedWhenClosed:"), false) };
unsafe { ffi::msg_set_bool(window, ffi::sel(c"setRestorable:"), false) };
unsafe {
crcbl_core::log::debug!(
"create: mode {:?} asked {:?} -> content {content:?}, mask {mask:#x}; screen \
frame {:?} visible {:?}; window frame {:?}, isRestorable {}, \
frameAutosaveName {:?}",
desc.mode,
desc.size,
target.map(|screen| screen.frame),
target.map(|screen| screen.visible),
ffi::msg_rect(window, ffi::sel(c"frame")),
ffi::msg_bool(window, ffi::sel(c"isRestorable")),
ffi::string_from_nsstring(ffi::msg(window, ffi::sel(c"frameAutosaveName"))),
);
}
let view = unsafe {
let allocated = ffi::msg(view_class, ffi::sel(c"alloc"));
let init: unsafe extern "C" fn(Id, Sel, NSRect) -> Id = ffi::msg_send();
init(
allocated,
ffi::sel(c"initWithFrame:"),
NSRect::new(0.0, 0.0, content.size.width, content.size.height),
)
};
if view.is_null() {
unsafe { ffi::msg_void(window, ffi::sel(c"release")) };
return Err(ShellError::WindowCreation(
"-[NSView initWithFrame:] returned nil".to_string(),
));
}
let layer = unsafe { ffi::msg(layer_class, ffi::sel(c"layer")) };
if layer.is_null() {
unsafe {
ffi::msg_void(view, ffi::sel(c"release"));
ffi::msg_void(window, ffi::sel(c"release"));
}
return Err(ShellError::WindowCreation(
"+[CAMetalLayer layer] returned nil".to_string(),
));
}
unsafe { ffi::msg_void(layer, ffi::sel(c"retain")) };
unsafe {
ffi::msg1_void(view, ffi::sel(c"setLayer:"), layer);
ffi::msg_set_bool(view, ffi::sel(c"setWantsLayer:"), true);
ffi::msg1_void(window, ffi::sel(c"setContentView:"), view);
ffi::msg_void(view, ffi::sel(c"release"));
}
unsafe {
ffi::msg_set_bool(window, ffi::sel(c"setAcceptsMouseMovedEvents:"), true);
focus_content_view(window);
view::add_tracking_area(view);
if desc.accept_drops {
view::register_dragged_types(view);
}
}
if let Some(title) = unsafe { ffi::nsstring(desc.title) } {
unsafe { ffi::msg1_void(window, ffi::sel(c"setTitle:"), title) };
} else {
return Err(ShellError::InvalidDescriptor(
"title contains a NUL byte".to_string(),
));
}
unsafe { apply_constraints(window, desc.constraints) };
if let Some(panel) = panel {
unsafe { Self::configure_panel(window, panel) };
}
if let Kind::Popup { input, .. } = kind {
unsafe { Self::configure_popup(window, input) };
}
if desc.transparent {
unsafe { make_transparent(window, layer) };
}
let scale = unsafe { backing_scale(window) };
unsafe { size_layer(layer, view, scale) };
if desc.visible && !matches!(kind, Kind::Popup { .. }) {
match panel {
Some(panel) => unsafe { Self::show_panel(window, panel.keyboard) },
None => unsafe {
ffi::msg1_void(window, ffi::sel(c"makeKeyAndOrderFront:"), ptr::null_mut());
},
}
}
Ok(Created {
window,
view,
layer,
scale,
size: unsafe { backing_size(view) },
})
}
pub(super) fn apply_mode(
&mut self,
handle: crate::WindowId,
mode: DisplayMode,
) -> Result<(), ShellError> {
let state = self.window(handle)?;
let window = state.window;
let resizable = state.resizable;
let saved = state.saved;
let target = match mode {
DisplayMode::Borderless { monitor } => Some(self.borderless_target(window, monitor)?),
DisplayMode::Windowed => None,
};
if target.is_some() && saved.is_none() {
let captured = unsafe {
super::shell::Saved {
mask: ffi::msg_usize(window, ffi::sel(c"styleMask")),
frame: ffi::msg_rect(window, ffi::sel(c"frame")),
}
};
crcbl_core::log::debug!("borderless: saved the windowed placement {captured:?}");
self.window_mut(handle)?.saved = Some(captured);
}
let (new_mask, new_frame) = match target {
Some(screen) => (geometry::style_mask(mode, resizable), screen.frame),
None => {
let restored = match self.window_mut(handle)?.saved.take() {
Some(saved) => saved,
None => {
let state = self.window(handle)?;
let mask = geometry::style_mask(DisplayMode::Windowed, resizable);
let size = geometry::points(state.requested_size);
let frame =
frame_for_content(mask, NSRect::new(0.0, 0.0, size.width, size.height));
let wanted = frame.size;
let area = self
.screen_of(window)
.and_then(|id| self.screens.iter().find(|screen| screen.id == id))
.map_or(
NSRect::new(0.0, 0.0, wanted.width, wanted.height),
|screen| geometry::centred(screen.visible, wanted),
);
super::shell::Saved { mask, frame: area }
}
};
(restored.mask, restored.frame)
}
};
let state = self.window_mut(handle)?;
state.effective_mode = match target {
Some(screen) => DisplayMode::Borderless {
monitor: Some(screen.id),
},
None => DisplayMode::Windowed,
};
unsafe {
ffi::msg_set_usize(window, ffi::sel(c"setStyleMask:"), new_mask);
crcbl_core::log::debug!(
"apply_mode: after setStyleMask: asked {new_mask:#x}, carries {:#x}, frame {:?}, \
view has the keyboard {}",
ffi::msg_usize(window, ffi::sel(c"styleMask")),
ffi::msg_rect(window, ffi::sel(c"frame")),
view_has_focus(window)
);
}
self.refresh_presentation();
crcbl_core::log::debug!(
"apply_mode: after refresh_presentation (options {:?}) the frame is {:?}",
unsafe { presentation_options() }.map(|options| format!("{options:#x}")),
unsafe { ffi::msg_rect(window, ffi::sel(c"frame")) }
);
unsafe {
set_frame(window, new_frame);
if target.is_some() && ffi::msg_bool(window, ffi::sel(c"isVisible")) {
ffi::msg1_void(window, ffi::sel(c"makeKeyAndOrderFront:"), ptr::null_mut());
}
focus_content_view(window);
}
let scale = unsafe { backing_scale(window) };
let state = self.window_mut(handle)?;
state.scale = scale;
unsafe { size_layer(state.layer, state.view, scale) };
crcbl_core::log::debug!(
"apply_mode: {mode:?} done — frame {:?}, view has the keyboard {}",
unsafe { ffi::msg_rect(window, ffi::sel(c"frame")) },
unsafe { view_has_focus(window) }
);
Ok(())
}
fn borderless_target(
&self,
window: Id,
monitor: Option<MonitorId>,
) -> Result<Screen, ShellError> {
if let Some(monitor) = monitor {
return self
.screens
.iter()
.find(|screen| screen.id == monitor)
.copied()
.ok_or(ShellError::NoSuchMonitor(monitor.0));
}
self.screen_of(window)
.and_then(|id| self.screens.iter().find(|screen| screen.id == id))
.or_else(|| self.screens.first())
.copied()
.ok_or_else(|| {
ShellError::Backend(
"there is no display to place a borderless window on".to_string(),
)
})
}
}
unsafe fn presentation_options() -> Option<usize> {
let class = ffi::class(c"NSApplication")?;
let app = unsafe { ffi::msg(class, ffi::sel(c"sharedApplication")) };
(!app.is_null()).then(|| unsafe { ffi::msg_usize(app, ffi::sel(c"presentationOptions")) })
}
unsafe fn focus_content_view(window: Id) -> bool {
let view = unsafe { ffi::msg(window, ffi::sel(c"contentView")) };
let taken = unsafe {
let make: unsafe extern "C" fn(Id, Sel, Id) -> ObjcBool = ffi::msg_send();
make(window, ffi::sel(c"makeFirstResponder:"), view) != ffi::NO
};
if !taken {
crcbl_core::log::warn!(
"-[NSWindow makeFirstResponder:] refused the content view; this window will receive \
no key events"
);
}
taken
}
unsafe fn view_has_focus(window: Id) -> bool {
unsafe {
let responder = ffi::msg(window, ffi::sel(c"firstResponder"));
let content = ffi::msg(window, ffi::sel(c"contentView"));
!responder.is_null() && responder == content
}
}
unsafe fn set_frame(window: Id, frame: NSRect) {
let before = unsafe { ffi::msg_rect(window, ffi::sel(c"frame")) };
let send: unsafe extern "C" fn(Id, Sel, NSRect, ObjcBool) = unsafe { ffi::msg_send() };
unsafe { send(window, ffi::sel(c"setFrame:display:"), frame, YES) };
let applied = unsafe { ffi::msg_rect(window, ffi::sel(c"frame")) };
crcbl_core::log::debug!("setFrame:display: from {before:?} asked {frame:?} landed {applied:?}");
if !frame_was_honoured(before, frame, applied) {
crcbl_core::log::warn!(
"-[NSWindow setFrame:display:] did not take: the window was at {before:?}, was asked \
for {frame:?}, and is at {applied:?}. Either the size did not arrive, or the origin \
stayed exactly where it was — CrcblWindow overrides constrainFrameRect:toScreen: to \
stop AppKit moving the origin, so if that override is installed the argument itself \
is the suspect. A position a fraction of a point from the request is AppKit aligning \
the frame to the display, and is not what this reports"
);
}
}
fn frame_was_honoured(before: NSRect, asked: NSRect, applied: NSRect) -> bool {
let size_arrived =
applied.size.width == asked.size.width && applied.size.height == asked.size.height;
let origin_dropped = applied.origin.x == before.origin.x
&& applied.origin.y == before.origin.y
&& (asked.origin.x != before.origin.x || asked.origin.y != before.origin.y);
size_arrived && !origin_dropped
}
pub(super) unsafe fn configuration_of(state: &AppWindow) -> crate::WindowConfiguration {
crate::WindowConfiguration {
size: unsafe { backing_size(state.view) },
scale_factor: state.scale,
mode: state.effective_mode,
}
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
fn the_window_class_installs_every_override_a_borderless_window_needs() {
let class = window_class().expect("CrcblWindow is built from runtime calls alone");
for selector in [
c"canBecomeKeyWindow",
c"canBecomeMainWindow",
c"constrainFrameRect:toScreen:",
] {
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, "CrcblWindow does not implement {selector:?}");
}
}
#[test]
fn a_frame_appkit_aligned_is_honoured() {
let before = NSRect::new(0.0, 0.0, 2056.0, 1361.0);
let asked = NSRect::new(708.0, 389.5, 640.0, 512.0);
let applied = NSRect::new(708.0, 389.0, 640.0, 512.0);
assert!(
frame_was_honoured(before, asked, applied),
"a fraction of a point is AppKit aligning the frame, not dropping the origin"
);
}
#[test]
fn an_origin_left_where_it_was_is_not_honoured() {
let before = NSRect::new(0.0, 0.0, 2056.0, 1361.0);
let asked = NSRect::new(708.0, 389.5, 640.0, 512.0);
let applied = NSRect::new(0.0, 0.0, 640.0, 512.0);
assert!(!frame_was_honoured(before, asked, applied));
}
#[test]
fn a_size_that_did_not_arrive_is_not_honoured() {
let before = NSRect::new(0.0, 0.0, 640.0, 480.0);
let asked = NSRect::new(0.0, 0.0, 1024.0, 768.0);
let applied = NSRect::new(0.0, 0.0, 640.0, 480.0);
assert!(!frame_was_honoured(before, asked, applied));
}
#[test]
fn a_placement_that_changes_nothing_is_honoured() {
let same = NSRect::new(100.0, 200.0, 640.0, 480.0);
assert!(frame_was_honoured(same, same, same));
}
}