#![allow(non_snake_case)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2::{define_class, msg_send, DefinedClass, MainThreadOnly};
use objc2::{AllocAnyThread, MainThreadMarker};
use objc2_app_kit::{
NSBackingStoreType, NSColor, NSCursor, NSPanel, NSPopUpMenuWindowLevel, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSVisualEffectBlendingMode, NSVisualEffectMaterial,
NSVisualEffectState, NSVisualEffectView, NSWindowDelegate, NSWindowStyleMask,
};
use objc2_foundation::{NSNotification, NSObjectProtocol, NSPoint, NSRect, NSSize};
use super::input::translate_ns_key;
use super::{UiEvent, WindowKind};
fn debug_cursor_enabled() -> bool {
static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*FLAG.get_or_init(|| std::env::var_os("MURI_DEBUG_CURSOR").is_some())
}
define_class!(
#[unsafe(super(NSView))]
#[thread_kind = MainThreadOnly]
#[name = "MuriContentView"]
#[ivars = WindowKind]
pub(super) struct MuriView;
impl MuriView {
#[unsafe(method(isFlipped))]
fn is_flipped(&self) -> bool {
true
}
#[unsafe(method(acceptsFirstResponder))]
fn accepts_first_responder(&self) -> bool {
true
}
#[unsafe(method(needsPanelToBecomeKey))]
fn needs_panel_to_become_key(&self) -> bool {
true
}
#[unsafe(method(acceptsFirstMouse:))]
fn accepts_first_mouse(&self, _event: Option<&objc2_app_kit::NSEvent>) -> bool {
true
}
#[unsafe(method(mouseMoved:))]
fn mouse_moved(&self, event: &objc2_app_kit::NSEvent) {
NSCursor::arrowCursor().set();
let (x, y) = view_point(self, event);
super::push_event(UiEvent::MouseMoved {
kind: *self.ivars(),
x,
y,
});
}
#[unsafe(method(resetCursorRects))]
fn reset_cursor_rects(&self) {
let bounds = self.bounds();
self.addCursorRect_cursor(bounds, &NSCursor::arrowCursor());
}
#[unsafe(method(cursorUpdate:))]
fn cursor_update(&self, _event: &objc2_app_kit::NSEvent) {
NSCursor::arrowCursor().set();
if debug_cursor_enabled() {
let key = self.window().map(|w| w.isKeyWindow()).unwrap_or(false);
eprintln!("MURI_CURSOR handler=cursorUpdate window_key={key} kind={:?}", self.ivars());
}
}
#[unsafe(method(mouseDragged:))]
fn mouse_dragged(&self, event: &objc2_app_kit::NSEvent) {
let (x, y) = view_point(self, event);
super::push_event(UiEvent::MouseMoved {
kind: *self.ivars(),
x,
y,
});
}
#[unsafe(method(mouseEntered:))]
fn mouse_entered(&self, _event: &objc2_app_kit::NSEvent) {
NSCursor::arrowCursor().set();
if debug_cursor_enabled() {
let key = self.window().map(|w| w.isKeyWindow()).unwrap_or(false);
eprintln!("MURI_CURSOR handler=mouseEntered window_key={key} kind={:?}", self.ivars());
}
}
#[unsafe(method(mouseExited:))]
fn mouse_exited(&self, _event: &objc2_app_kit::NSEvent) {
super::push_event(UiEvent::MouseExited {
kind: *self.ivars(),
});
}
#[unsafe(method(mouseDown:))]
fn mouse_down(&self, event: &objc2_app_kit::NSEvent) {
let (x, y) = view_point(self, event);
super::push_event(UiEvent::MouseDown {
kind: *self.ivars(),
x,
y,
});
}
#[unsafe(method(keyDown:))]
fn key_down(&self, event: &objc2_app_kit::NSEvent) {
if let Some(key) = translate_ns_key(event) {
super::push_event(UiEvent::Key(key));
}
}
}
);
impl MuriView {
fn new(mtm: MainThreadMarker, frame: NSRect, kind: WindowKind) -> Retained<Self> {
let this = mtm.alloc::<Self>().set_ivars(kind);
unsafe { msg_send![super(this), initWithFrame: frame] }
}
}
define_class!(
#[unsafe(super(objc2_foundation::NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "MuriWindowDelegate"]
#[ivars = WindowKind]
pub(super) struct MuriWindowDelegate;
unsafe impl NSObjectProtocol for MuriWindowDelegate {}
unsafe impl NSWindowDelegate for MuriWindowDelegate {
#[unsafe(method(windowDidBecomeKey:))]
fn did_become_key(&self, _n: &NSNotification) {
super::push_event(UiEvent::FocusChanged {
kind: *self.ivars(),
key: true,
});
}
#[unsafe(method(windowDidResignKey:))]
fn did_resign_key(&self, _n: &NSNotification) {
super::push_event(UiEvent::FocusChanged {
kind: *self.ivars(),
key: false,
});
}
}
);
impl MuriWindowDelegate {
fn new(mtm: MainThreadMarker, kind: WindowKind) -> Retained<Self> {
let this = mtm.alloc::<Self>().set_ivars(kind);
unsafe { msg_send![super(this), init] }
}
}
pub(super) fn view_point(view: &NSView, event: &objc2_app_kit::NSEvent) -> (f64, f64) {
let p = view.convertPoint_fromView(event.locationInWindow(), None);
(p.x, p.y)
}
pub(super) struct NativePanel {
pub panel: Retained<NSPanel>,
pub view: Retained<MuriView>,
pub delegate: Retained<MuriWindowDelegate>,
}
pub(super) fn make_panel(
mtm: MainThreadMarker,
content_rect: NSRect,
corner_radius: f32,
kind: WindowKind,
) -> NativePanel {
let style = NSWindowStyleMask::Borderless | NSWindowStyleMask::NonactivatingPanel;
let panel: Retained<NSPanel> = unsafe {
msg_send![
mtm.alloc::<NSPanel>(),
initWithContentRect: content_rect,
styleMask: style,
backing: NSBackingStoreType::Buffered,
defer: false,
]
};
unsafe {
panel.setReleasedWhenClosed(false);
}
panel.setLevel(NSPopUpMenuWindowLevel);
panel.setFloatingPanel(true);
panel.setBecomesKeyOnlyIfNeeded(true);
panel.setHidesOnDeactivate(false);
panel.setOpaque(false);
panel.setHasShadow(true);
panel.setBackgroundColor(Some(&NSColor::clearColor()));
panel.setAcceptsMouseMovedEvents(true);
let bounds = NSRect::new(NSPoint::new(0.0, 0.0), content_rect.size);
let effect = NSVisualEffectView::initWithFrame(mtm.alloc(), bounds);
effect.setMaterial(NSVisualEffectMaterial::Menu);
effect.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow);
effect.setState(NSVisualEffectState::Active);
effect.setWantsLayer(true);
if let Some(layer) = effect.layer() {
layer.setCornerRadius(corner_radius as f64);
layer.setMasksToBounds(true);
}
let view = MuriView::new(mtm, bounds, kind);
view.setWantsLayer(true);
if let Some(layer) = view.layer() {
layer.setCornerRadius(corner_radius as f64);
layer.setMasksToBounds(true);
layer.setOpaque(false);
}
effect.addSubview(&view);
panel.setContentView(Some(&effect));
let options = NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::CursorUpdate
| NSTrackingAreaOptions::ActiveAlways
| NSTrackingAreaOptions::InVisibleRect;
let tracking: Retained<NSTrackingArea> = unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0)),
options,
Some(&view),
None,
)
};
view.addTrackingArea(&tracking);
let delegate = MuriWindowDelegate::new(mtm, kind);
let proto: &ProtocolObject<dyn NSWindowDelegate> = ProtocolObject::from_ref(&*delegate);
panel.setDelegate(Some(proto));
panel.setInitialFirstResponder(Some(&view));
NativePanel {
panel,
view,
delegate,
}
}