use core::ptr::NonNull;
use std::cell::{Cell, RefCell};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, ProtocolObject, Sel};
use objc2::{AllocAnyThread, DefinedClass, MainThreadOnly, Message, define_class, msg_send, sel};
use objc2_app_kit::{
NSAppearanceCustomization, NSEvent, NSEventModifierFlags, NSResponder, NSTextInputClient,
NSTrackingArea, NSTrackingAreaOptions, NSView,
};
use objc2_core_foundation::CGFloat;
use objc2_foundation::{
MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSDefaultRunLoopMode,
NSObjectProtocol, NSPoint, NSRange, NSRect, NSRunLoop, NSRunLoopCommonModes, NSSize, NSString,
NSUInteger,
};
use objc2_foundation::{NSDictionary, NSNumber};
use objc2_io_surface::{
IOSurface, IOSurfaceLockOptions, IOSurfacePropertyKey, IOSurfacePropertyKeyBytesPerElement,
IOSurfacePropertyKeyHeight, IOSurfacePropertyKeyPixelFormat, IOSurfacePropertyKeyWidth,
IOSurfaceRef,
};
use objc2_quartz_core::{CADisplayLink, CALayer, CATransaction, kCAGravityTopLeft};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Modifiers {
pub shift: bool,
pub control: bool,
pub option: bool,
pub command: bool,
pub function: bool,
}
impl Modifiers {
fn of(flags: NSEventModifierFlags) -> Modifiers {
Modifiers {
shift: flags.contains(NSEventModifierFlags::Shift),
control: flags.contains(NSEventModifierFlags::Control),
option: flags.contains(NSEventModifierFlags::Option),
command: flags.contains(NSEventModifierFlags::Command),
function: flags.contains(NSEventModifierFlags::Function),
}
}
}
#[derive(Clone, Debug)]
pub struct KeyPress {
pub key_code: u16,
pub characters: String,
pub unmodified: String,
pub modifiers: Modifiers,
pub repeat: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Handled {
Consumed,
Interpret,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Button {
Left,
Right,
Middle,
Other(u8),
}
#[derive(Clone, Copy, Debug)]
pub struct Mouse {
pub kind: MouseKind,
pub button: Button,
pub x: f64,
pub y: f64,
pub modifiers: Modifiers,
pub clicks: u8,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MouseKind {
Down,
Up,
Moved,
Dragged,
Entered,
Exited,
}
#[derive(Clone, Copy, Debug)]
pub struct Scroll {
pub dx: f64,
pub dy: f64,
pub precise: bool,
pub x: f64,
pub y: f64,
pub modifiers: Modifiers,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Focus {
Gained,
Lost,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Appearance {
Light,
Dark,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
}
pub trait Responder {
fn key(&mut self, key: &KeyPress) -> Handled;
fn text(&mut self, text: &str);
fn command(&mut self, selector: &str);
fn marked_text(&mut self, text: &str);
fn mouse(&mut self, event: &Mouse);
fn scroll(&mut self, event: &Scroll);
fn resized(&mut self, size: NSSize, scale: CGFloat);
fn frame(&mut self, target: f64);
fn focus(&mut self, focus: Focus);
fn cursor_rect(&self) -> Rect;
fn wake(&mut self) {}
fn presented(&mut self) {}
fn appearance(&mut self, appearance: Appearance) {
let _ = appearance;
}
}
#[derive(Clone, Copy)]
pub struct Waker {
view: NonNull<AnyObject>,
}
unsafe impl Send for Waker {}
unsafe impl Sync for Waker {}
impl Waker {
pub fn wake(&self) {
unsafe {
let modes = NSArray::from_slice(&[NSRunLoopCommonModes]);
let _: () = msg_send![
self.view.as_ptr(),
performSelectorOnMainThread: sel!(wake:),
withObject: core::ptr::null::<AnyObject>(),
waitUntilDone: false,
modes: &*modes,
];
}
}
pub fn present(&self, surface: &SurfaceHandle) {
unsafe {
let modes = NSArray::from_slice(&[NSRunLoopCommonModes]);
let _: () = msg_send![
self.view.as_ptr(),
performSelectorOnMainThread: sel!(presentSurface:),
withObject: surface.0.as_ptr(),
waitUntilDone: false,
modes: &*modes,
];
}
}
}
pub struct SurfaceHandle(NonNull<IOSurface>);
unsafe impl Send for SurfaceHandle {}
unsafe impl Sync for SurfaceHandle {}
impl Drop for SurfaceHandle {
fn drop(&mut self) {
unsafe { objc2::ffi::objc_release(self.0.as_ptr().cast()) };
}
}
pub struct DrawableIvars {
responder: RefCell<Option<Box<dyn Responder>>>,
link: RefCell<Option<Retained<CADisplayLink>>>,
tracking: RefCell<Option<Retained<NSTrackingArea>>>,
marked: RefCell<String>,
scale: Cell<CGFloat>,
}
define_class!(
#[unsafe(super(NSView, NSResponder))]
#[thread_kind = MainThreadOnly]
#[name = "MacosLiquidGlassDrawableView"]
#[ivars = DrawableIvars]
pub struct DrawableView;
impl DrawableView {
#[unsafe(method(isFlipped))]
fn is_flipped(&self) -> bool {
true
}
#[unsafe(method(acceptsFirstResponder))]
fn accepts_first_responder(&self) -> bool {
true
}
#[unsafe(method_id(makeBackingLayer))]
fn make_backing_layer(&self) -> Retained<CALayer> {
let layer = CALayer::new();
layer.setOpaque(false);
layer.setContentsGravity(unsafe { kCAGravityTopLeft });
layer.setContentsScale(self.ivars().scale.get());
layer
}
#[unsafe(method(wantsUpdateLayer))]
fn wants_update_layer(&self) -> bool {
true
}
#[unsafe(method(updateLayer))]
fn update_layer(&self) {}
#[unsafe(method(viewDidChangeBackingProperties))]
fn view_did_change_backing_properties(&self) {
let _: () = unsafe { msg_send![super(self), viewDidChangeBackingProperties] };
self.refresh_scale();
self.notify_resized();
}
#[unsafe(method(setFrameSize:))]
fn set_frame_size(&self, size: NSSize) {
let _: () = unsafe { msg_send![super(self), setFrameSize: size] };
self.notify_resized();
}
#[unsafe(method(viewDidChangeEffectiveAppearance))]
fn view_did_change_effective_appearance(&self) {
let _: () = unsafe { msg_send![super(self), viewDidChangeEffectiveAppearance] };
let appearance = if crate::is_dark(&self.effectiveAppearance()) {
Appearance::Dark
} else {
Appearance::Light
};
self.with_responder(|r| r.appearance(appearance));
}
#[unsafe(method(viewDidMoveToWindow))]
fn view_did_move_to_window(&self) {
let _: () = unsafe { msg_send![super(self), viewDidMoveToWindow] };
self.refresh_scale();
self.notify_resized();
}
#[unsafe(method(becomeFirstResponder))]
fn become_first_responder(&self) -> bool {
let ok: bool = unsafe { msg_send![super(self), becomeFirstResponder] };
if ok {
self.with_responder(|r| r.focus(Focus::Gained));
}
ok
}
#[unsafe(method(resignFirstResponder))]
fn resign_first_responder(&self) -> bool {
let ok: bool = unsafe { msg_send![super(self), resignFirstResponder] };
if ok {
self.with_responder(|r| r.focus(Focus::Lost));
}
ok
}
#[unsafe(method(keyDown:))]
fn key_down(&self, event: &NSEvent) {
let press = KeyPress {
key_code: event.keyCode(),
characters: event.characters().map(|s| s.to_string()).unwrap_or_default(),
unmodified: event
.charactersIgnoringModifiers()
.map(|s| s.to_string())
.unwrap_or_default(),
modifiers: Modifiers::of(event.modifierFlags()),
repeat: event.isARepeat(),
};
let handled = self
.with_responder(|r| r.key(&press))
.unwrap_or(Handled::Interpret);
if handled == Handled::Interpret {
let events = NSArray::from_slice(&[event]);
self.interpretKeyEvents(&events);
}
}
#[unsafe(method(flagsChanged:))]
fn flags_changed(&self, _event: &NSEvent) {}
#[unsafe(method(copy:))]
fn copy_(&self, _sender: Option<&AnyObject>) {
self.with_responder(|r| r.command("copy:"));
}
#[unsafe(method(paste:))]
fn paste_(&self, _sender: Option<&AnyObject>) {
self.with_responder(|r| r.command("paste:"));
}
#[unsafe(method(zoomIn:))]
fn zoom_in(&self, _sender: Option<&AnyObject>) {
self.with_responder(|r| r.command("zoomIn:"));
}
#[unsafe(method(zoomOut:))]
fn zoom_out(&self, _sender: Option<&AnyObject>) {
self.with_responder(|r| r.command("zoomOut:"));
}
#[unsafe(method(zoomActual:))]
fn zoom_actual(&self, _sender: Option<&AnyObject>) {
self.with_responder(|r| r.command("zoomActual:"));
}
#[unsafe(method(mouseDown:))]
fn mouse_down(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Down);
}
#[unsafe(method(mouseUp:))]
fn mouse_up(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Up);
}
#[unsafe(method(rightMouseDown:))]
fn right_mouse_down(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Down);
}
#[unsafe(method(rightMouseUp:))]
fn right_mouse_up(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Up);
}
#[unsafe(method(otherMouseDown:))]
fn other_mouse_down(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Down);
}
#[unsafe(method(otherMouseUp:))]
fn other_mouse_up(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Up);
}
#[unsafe(method(mouseMoved:))]
fn mouse_moved(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Moved);
}
#[unsafe(method(mouseDragged:))]
fn mouse_dragged(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Dragged);
}
#[unsafe(method(rightMouseDragged:))]
fn right_mouse_dragged(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Dragged);
}
#[unsafe(method(otherMouseDragged:))]
fn other_mouse_dragged(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Dragged);
}
#[unsafe(method(mouseEntered:))]
fn mouse_entered(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Entered);
}
#[unsafe(method(mouseExited:))]
fn mouse_exited(&self, event: &NSEvent) {
self.forward_mouse(event, MouseKind::Exited);
}
#[unsafe(method(scrollWheel:))]
fn scroll_wheel(&self, event: &NSEvent) {
let p = self.local_point(event);
let scroll = Scroll {
dx: event.scrollingDeltaX(),
dy: event.scrollingDeltaY(),
precise: event.hasPreciseScrollingDeltas(),
x: p.x,
y: p.y,
modifiers: Modifiers::of(event.modifierFlags()),
};
self.with_responder(|r| r.scroll(&scroll));
}
#[unsafe(method(updateTrackingAreas))]
fn update_tracking_areas(&self) {
let _: () = unsafe { msg_send![super(self), updateTrackingAreas] };
if let Some(old) = self.ivars().tracking.borrow_mut().take() {
self.removeTrackingArea(&old);
}
let options = NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::ActiveAlways
| NSTrackingAreaOptions::InVisibleRect;
let area = unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
self.bounds(),
options,
Some(self),
None,
)
};
self.addTrackingArea(&area);
*self.ivars().tracking.borrow_mut() = Some(area);
}
#[unsafe(method(tick:))]
fn tick(&self, link: &CADisplayLink) {
let target = link.targetTimestamp();
self.with_responder(|r| r.frame(target));
}
#[unsafe(method(wake:))]
fn wake(&self, _ignored: Option<&AnyObject>) {
self.with_responder(|r| r.wake());
}
#[unsafe(method(presentSurface:))]
fn present_surface(&self, surface: &IOSurface) {
self.set_surface(surface);
self.with_responder(|r| r.presented());
}
}
unsafe impl NSObjectProtocol for DrawableView {}
unsafe impl NSTextInputClient for DrawableView {
#[unsafe(method(insertText:replacementRange:))]
unsafe fn insert_text_replacement_range(&self, text: &AnyObject, _range: NSRange) {
let s = string_of(text);
self.ivars().marked.borrow_mut().clear();
self.with_responder(|r| {
r.marked_text("");
r.text(&s);
});
}
#[unsafe(method(doCommandBySelector:))]
unsafe fn do_command_by_selector(&self, selector: Sel) {
let name = selector.name().to_str().unwrap_or("").to_string();
self.with_responder(|r| r.command(&name));
}
#[unsafe(method(setMarkedText:selectedRange:replacementRange:))]
unsafe fn set_marked_text_selected_range_replacement_range(
&self,
text: &AnyObject,
_selected: NSRange,
_replacement: NSRange,
) {
let s = string_of(text);
*self.ivars().marked.borrow_mut() = s.clone();
self.with_responder(|r| r.marked_text(&s));
}
#[unsafe(method(unmarkText))]
fn unmark_text(&self) {
self.ivars().marked.borrow_mut().clear();
self.with_responder(|r| r.marked_text(""));
}
#[unsafe(method(selectedRange))]
fn selected_range(&self) -> NSRange {
NSRange::new(NSUInteger::MAX, 0)
}
#[unsafe(method(markedRange))]
fn marked_range(&self) -> NSRange {
let n = self.ivars().marked.borrow().encode_utf16().count();
if n == 0 {
NSRange::new(NSUInteger::MAX, 0)
} else {
NSRange::new(0, n)
}
}
#[unsafe(method(hasMarkedText))]
fn has_marked_text(&self) -> bool {
!self.ivars().marked.borrow().is_empty()
}
#[unsafe(method_id(attributedSubstringForProposedRange:actualRange:))]
unsafe fn attributed_substring_for_proposed_range_actual_range(
&self,
_range: NSRange,
_actual: *mut NSRange,
) -> Option<Retained<NSAttributedString>> {
None
}
#[unsafe(method_id(validAttributesForMarkedText))]
fn valid_attributes_for_marked_text(&self) -> Retained<NSArray<NSAttributedStringKey>> {
NSArray::new()
}
#[unsafe(method(firstRectForCharacterRange:actualRange:))]
unsafe fn first_rect_for_character_range_actual_range(
&self,
_range: NSRange,
_actual: *mut NSRange,
) -> NSRect {
let r = self
.with_responder(|r| r.cursor_rect())
.unwrap_or_default();
let flipped_y = self.bounds().size.height - r.y - r.h;
let local = NSRect::new(NSPoint::new(r.x, flipped_y), NSSize::new(r.w, r.h));
let in_window = self.convertRect_toView(local, None);
match self.window() {
Some(w) => w.convertRectToScreen(in_window),
None => in_window,
}
}
#[unsafe(method(characterIndexForPoint:))]
fn character_index_for_point(&self, _point: NSPoint) -> NSUInteger {
NSUInteger::MAX
}
}
);
fn string_of(text: &AnyObject) -> String {
if let Some(s) = text.downcast_ref::<NSString>() {
return s.to_string();
}
if let Some(a) = text.downcast_ref::<NSAttributedString>() {
return a.string().to_string();
}
String::new()
}
impl DrawableView {
pub fn new(mtm: MainThreadMarker, frame: NSRect) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(DrawableIvars {
responder: RefCell::new(None),
link: RefCell::new(None),
tracking: RefCell::new(None),
marked: RefCell::new(String::new()),
scale: Cell::new(1.0),
});
let this: Retained<Self> = unsafe { msg_send![super(this), initWithFrame: frame] };
this.setWantsLayer(true);
this
}
pub fn set_responder(&self, responder: Box<dyn Responder>) {
*self.ivars().responder.borrow_mut() = Some(responder);
}
fn with_responder<R>(&self, f: impl FnOnce(&mut dyn Responder) -> R) -> Option<R> {
let mut slot = self.ivars().responder.borrow_mut();
slot.as_mut().map(|r| f(r.as_mut()))
}
pub fn backing_scale_factor(&self) -> CGFloat {
self.ivars().scale.get()
}
fn refresh_scale(&self) {
let scale = self.window().map_or(1.0, |w| w.backingScaleFactor());
self.ivars().scale.set(scale);
if let Some(layer) = self.layer() {
CATransaction::begin();
CATransaction::setDisableActions(true);
layer.setContentsScale(scale);
CATransaction::commit();
}
}
fn notify_resized(&self) {
let size = self.bounds().size;
let scale = self.ivars().scale.get();
self.with_responder(|r| r.resized(size, scale));
}
pub fn set_surface(&self, surface: &IOSurface) {
if let Some(layer) = self.layer() {
CATransaction::begin();
CATransaction::setDisableActions(true);
unsafe { layer.setContents(Some(surface)) };
CATransaction::commit();
}
}
pub fn start_display_link(&self) {
self.stop_display_link();
let link = unsafe { self.displayLinkWithTarget_selector(self, sel!(tick:)) };
unsafe {
link.addToRunLoop_forMode(&NSRunLoop::mainRunLoop(), NSRunLoopCommonModes);
link.addToRunLoop_forMode(&NSRunLoop::mainRunLoop(), NSDefaultRunLoopMode);
}
*self.ivars().link.borrow_mut() = Some(link);
}
pub fn waker(&self) -> Waker {
let retained: Retained<DrawableView> = self.retain();
let ptr = Retained::into_raw(retained).cast::<AnyObject>();
Waker {
view: NonNull::new(ptr).expect("a retained object is not null"),
}
}
pub fn stop_display_link(&self) {
if let Some(link) = self.ivars().link.borrow_mut().take() {
link.invalidate();
}
}
fn local_point(&self, event: &NSEvent) -> NSPoint {
let p = self.convertPoint_fromView(event.locationInWindow(), None);
p
}
fn forward_mouse(&self, event: &NSEvent, kind: MouseKind) {
let p = self.local_point(event);
let button = match event.buttonNumber() {
0 => Button::Left,
1 => Button::Right,
2 => Button::Middle,
n => Button::Other(u8::try_from(n).unwrap_or(u8::MAX)),
};
let clicks = match kind {
MouseKind::Down | MouseKind::Up => u8::try_from(event.clickCount()).unwrap_or(1),
_ => 0,
};
let m = Mouse {
kind,
button,
x: p.x,
y: p.y,
modifiers: Modifiers::of(event.modifierFlags()),
clicks,
};
self.with_responder(|r| r.mouse(&m));
}
}
impl Drop for DrawableIvars {
fn drop(&mut self) {
if let Some(link) = self.link.get_mut().take() {
link.invalidate();
}
}
}
pub fn as_text_input_client(view: &DrawableView) -> &ProtocolObject<dyn NSTextInputClient> {
ProtocolObject::from_ref(view)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ColorSpace {
#[default]
Srgb,
DisplayP3,
}
pub struct Surface {
inner: Retained<IOSurface>,
width: usize,
height: usize,
stride: usize,
}
impl Surface {
const BGRA: u32 = 0x4247_5241;
pub fn new(width: usize, height: usize) -> Option<Surface> {
Surface::in_space(width, height, ColorSpace::Srgb)
}
pub fn in_space(width: usize, height: usize, space: ColorSpace) -> Option<Surface> {
if width == 0 || height == 0 {
return None;
}
let number = |v: usize| -> Retained<AnyObject> {
let n = NSNumber::new_usize(v);
Retained::into_super(Retained::into_super(Retained::into_super(n)))
};
let (k_w, k_h, k_bpe, k_fmt): (
&IOSurfacePropertyKey,
&IOSurfacePropertyKey,
&IOSurfacePropertyKey,
&IOSurfacePropertyKey,
) = unsafe {
(
IOSurfacePropertyKeyWidth,
IOSurfacePropertyKeyHeight,
IOSurfacePropertyKeyBytesPerElement,
IOSurfacePropertyKeyPixelFormat,
)
};
let values = [
number(width),
number(height),
number(4),
number(usize::try_from(Surface::BGRA).ok()?),
];
let props: Retained<NSDictionary<IOSurfacePropertyKey, AnyObject>> =
NSDictionary::from_retained_objects(&[k_w, k_h, k_bpe, k_fmt], &values);
let inner = IOSurface::initWithProperties(IOSurface::alloc(), &props)?;
let name = unsafe {
match space {
ColorSpace::Srgb => objc2_core_graphics::kCGColorSpaceSRGB,
ColorSpace::DisplayP3 => objc2_core_graphics::kCGColorSpaceDisplayP3,
}
};
let cg = objc2_core_graphics::CGColorSpace::with_name(Some(name))?;
let plist = cg.property_list()?;
unsafe {
let r: &IOSurfaceRef = core::ptr::NonNull::from(&*inner)
.cast::<IOSurfaceRef>()
.as_ref();
r.set_value(objc2_io_surface::kIOSurfaceColorSpace, &plist);
}
let stride = usize::try_from(inner.bytesPerRow()).ok()? / 4;
Some(Surface {
inner,
width,
height,
stride,
})
}
pub fn size(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn stride(&self) -> usize {
self.stride
}
pub fn io_surface(&self) -> &IOSurface {
&self.inner
}
pub fn handle(&self) -> SurfaceHandle {
let retained: Retained<IOSurface> = self.inner.clone();
SurfaceHandle(NonNull::new(Retained::into_raw(retained)).expect("retained"))
}
pub fn write(&self, f: impl FnOnce(&mut [u32], usize)) -> bool {
let opts = IOSurfaceLockOptions::empty();
if self.inner.lockWithOptions_seed(opts, std::ptr::null_mut()) != 0 {
return false;
}
let words = self.stride * self.height;
let pixels = unsafe {
std::slice::from_raw_parts_mut(self.inner.baseAddress().as_ptr().cast::<u32>(), words)
};
f(pixels, self.stride);
self.inner
.unlockWithOptions_seed(opts, std::ptr::null_mut());
true
}
}
#[cfg(test)]
mod tests {
use super::Surface;
#[test]
fn a_surface_takes_pixels_and_reports_its_stride() {
let s = Surface::new(5, 3).expect("the kernel makes a 5x3 BGRA surface");
assert_eq!(s.size(), (5, 3));
assert!(s.stride() >= 5, "{}", s.stride());
let stride = s.stride();
assert!(s.write(|px, st| {
assert_eq!(st, stride);
assert_eq!(px.len(), stride * 3);
px[st * 2 + 4] = 0xFF11_2233;
}));
assert!(s.write(|px, st| assert_eq!(px[st * 2 + 4], 0xFF11_2233)));
assert!(Surface::new(0, 3).is_none());
}
}