#![allow(unsafe_code)]
mod menu;
mod vsync;
use std::time::Duration;
use dispatch2::{DispatchQueue, DispatchQueueGlobalPriority, GlobalQueueIdentifier};
use inset_embedder::{Rect, WindowBackground, WindowConfig};
use objc2::rc::Retained;
use objc2::runtime::{AnyClass, AnyObject};
use objc2::{AnyThread, MainThreadMarker};
use objc2_app_kit::{
NSAnimatablePropertyContainer, NSAnimationContext, NSAutoresizingMaskOptions, NSEvent,
NSGlassEffectView, NSGlassEffectViewStyle, NSScreen, NSTrackingArea, NSTrackingAreaOptions,
NSView, NSVisualEffectBlendingMode, NSVisualEffectMaterial, NSVisualEffectState,
NSVisualEffectView, NSWindow, NSWindowCollectionBehavior, NSWindowOrderingMode,
};
use objc2_foundation::{NSPoint, NSRect, NSSize};
use objc2_quartz_core::{CAMediaTimingFunction, kCAMediaTimingFunctionEaseInEaseOut};
use winit::platform::macos::WindowAttributesExtMacOS;
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
use winit::window::{Window, WindowAttributes};
pub(crate) use menu::popup_menu;
pub(crate) use vsync::Vsync;
pub(crate) const FRAME_ON_RESIZE: bool = true;
fn ns_view(window: &Window) -> Option<Retained<NSView>> {
let handle = window.window_handle().ok()?;
let RawWindowHandle::AppKit(appkit) = handle.as_raw() else {
return None;
};
unsafe { Retained::retain(appkit.ns_view.cast::<NSView>().as_ptr()) }
}
fn ns_window(window: &Window) -> Option<Retained<NSWindow>> {
ns_view(window)?.window()
}
pub(crate) fn extend_attributes(
attributes: WindowAttributes,
config: &WindowConfig,
) -> WindowAttributes {
attributes.with_has_shadow(config.shadow)
}
pub(crate) fn configure(window: &Window, config: &WindowConfig) {
if !config.activating
&& let Some(view) = ns_view(window)
{
track_pointer_always(&view);
}
let Some(ns_window) = ns_window(window) else {
return;
};
if config.all_desktops {
ns_window.setCollectionBehavior(
NSWindowCollectionBehavior::CanJoinAllSpaces
| NSWindowCollectionBehavior::Stationary
| NSWindowCollectionBehavior::FullScreenAuxiliary,
);
}
if matches!(
config.background,
WindowBackground::Blurred | WindowBackground::Glass
) && let Some(mtm) = MainThreadMarker::new()
&& let Some(content) = ns_window.contentView()
&& let Some(frame_view) = unsafe { content.superview() }
{
let effect = background_view(mtm, config.background, content.frame());
effect.setAutoresizingMask(
NSAutoresizingMaskOptions::ViewWidthSizable
| NSAutoresizingMaskOptions::ViewHeightSizable,
);
frame_view.addSubview_positioned_relativeTo(
&effect,
NSWindowOrderingMode::Below,
Some(&content),
);
}
}
pub(crate) fn pressed_buttons() -> Option<i64> {
Some(NSEvent::pressedMouseButtons() as i64)
}
fn track_pointer_always(view: &NSView) {
let owner: &AnyObject = view.as_ref();
let area = unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
view.bounds(),
NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::ActiveAlways
| NSTrackingAreaOptions::InVisibleRect,
Some(owner),
None,
)
};
view.addTrackingArea(&area);
}
pub(crate) fn pointer_position(window: &Window) -> Option<[f64; 2]> {
let view = ns_view(window)?;
let ns_window = view.window()?;
let in_window = ns_window.convertPointFromScreen(NSEvent::mouseLocation());
let in_view = view.convertPoint_fromView(in_window, None);
let y = if view.isFlipped() {
in_view.y
} else {
view.bounds().size.height - in_view.y
};
let scale = ns_window.backingScaleFactor();
Some([in_view.x * scale, y * scale])
}
fn background_view(
mtm: MainThreadMarker,
background: WindowBackground,
frame: NSRect,
) -> Retained<NSView> {
if background == WindowBackground::Glass && AnyClass::get(c"NSGlassEffectView").is_some() {
let glass = NSGlassEffectView::initWithFrame(mtm.alloc(), frame);
glass.setStyle(NSGlassEffectViewStyle::Regular);
return Retained::into_super(glass);
}
let blur = NSVisualEffectView::initWithFrame(mtm.alloc(), frame);
blur.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow);
blur.setState(NSVisualEffectState::Active);
blur.setMaterial(NSVisualEffectMaterial::Popover);
Retained::into_super(blur)
}
pub(crate) fn set_frame(window: &Window, frame: Rect) -> bool {
let Some(ns_window) = ns_window(window) else {
return false;
};
let Some(rect) = appkit_rect(frame) else {
return false;
};
ns_window.setFrame_display(rect, true);
true
}
pub(crate) fn animate_frame(window: &Window, frame: Rect, duration: Duration) -> bool {
let Some(ns_window) = ns_window(window) else {
return false;
};
let Some(rect) = appkit_rect(frame) else {
return false;
};
NSAnimationContext::beginGrouping();
let context = NSAnimationContext::currentContext();
context.setDuration(duration.as_secs_f64());
let ease =
CAMediaTimingFunction::functionWithName(unsafe { kCAMediaTimingFunctionEaseInEaseOut });
context.setTimingFunction(Some(&ease));
ns_window.animator().setFrame_display(rect, true);
NSAnimationContext::endGrouping();
true
}
fn appkit_rect(frame: Rect) -> Option<NSRect> {
let mtm = MainThreadMarker::new()?;
let primary = NSScreen::screens(mtm).firstObject()?;
let screen_height = primary.frame().size.height;
Some(NSRect::new(
NSPoint::new(frame.left, screen_height - frame.top - frame.height()),
NSSize::new(frame.width(), frame.height()),
))
}
pub(crate) fn run_off_main(work: Box<dyn FnOnce() + Send>) {
let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::Priority(
DispatchQueueGlobalPriority::Default,
));
queue.exec_async(work);
}