use std::cell::{Cell, RefCell};
use objc2::rc::Retained;
use objc2::{AnyThread, DefinedClass, MainThreadOnly, define_class, msg_send};
use objc2_app_kit::{
NSApplication, NSBackingStoreType, NSColor, NSCursor, NSEvent, NSView, NSWindow,
NSWindowCollectionBehavior, NSWindowStyleMask,
};
use objc2_core_foundation::CFRetained;
use objc2_core_graphics::CGImage;
use objc2_foundation::{MainThreadMarker, NSPoint, NSRect};
use crate::{
Color, Hud, MonitorId, OverlayHandle, OverlayOps, PlatformError, PlatformEvent, Result,
};
use super::keymap::vkey_to_xkb_keysym;
pub(crate) struct OverlayResources {
pub window: Retained<OverlayWindow>,
pub view: Retained<OverlayView>,
}
pub(crate) fn create(monitor: MonitorId) -> Result<OverlayHandle> {
super::app::run_on_main_sync(move || -> Result<OverlayHandle> {
let mtm = MainThreadMarker::new().expect("create overlay on main");
let screen = super::monitor::ns_screen_for(monitor)
.ok_or(PlatformError::MonitorNotFound(monitor))?;
let frame = screen.frame();
let style = NSWindowStyleMask::Borderless;
let alloc = mtm.alloc::<OverlayWindow>();
let window: Retained<OverlayWindow> = unsafe {
msg_send![
alloc,
initWithContentRect: frame,
styleMask: style,
backing: NSBackingStoreType::Buffered,
defer: false,
]
};
window.setOpaque(false);
let clear = unsafe { NSColor::clearColor() };
window.setBackgroundColor(Some(&clear));
window.setLevel(NS_WINDOW_LEVEL_STATUS);
window.setCollectionBehavior(
NSWindowCollectionBehavior::CanJoinAllSpaces
| NSWindowCollectionBehavior::FullScreenAuxiliary
| NSWindowCollectionBehavior::Stationary,
);
window.setIgnoresMouseEvents(true);
window.setHasShadow(false);
window.setAcceptsMouseMovedEvents(true);
let ivars = OverlayIvars {
monitor,
tint: RefCell::new(Color::TRANSPARENT),
hud: RefCell::new(None),
static_hud_image: RefCell::new(None),
static_hud_hash: Cell::new(0),
dynamic_hud_image: RefCell::new(None),
background_image: RefCell::new(None),
cursor_hidden: RefCell::new(false),
pointing_hand: RefCell::new(false),
last_flags: RefCell::new(0),
transparent_cursor: RefCell::new(None),
captures_input: RefCell::new(false),
};
let view: Retained<OverlayView> = OverlayView::new(mtm, ivars);
view.setFrame(NSRect {
origin: NSPoint { x: 0.0, y: 0.0 },
size: frame.size,
});
window.setContentView(Some(&view));
window.makeFirstResponder(Some(&view));
install_tracking_area(&view, frame.size);
super::with_main_state(|s| {
s.overlays.insert(
monitor,
OverlayResources {
window: window.clone(),
view: view.clone(),
},
);
});
Ok(OverlayHandle::from_backend(MacOverlay { monitor }))
})
}
#[allow(dead_code)]
const NS_WINDOW_LEVEL_SCREEN_SAVER: isize = 1000;
const NS_WINDOW_LEVEL_STATUS: isize = 25;
struct MacOverlay {
monitor: MonitorId,
}
impl OverlayOps for MacOverlay {
fn show(&mut self) {
let monitor = self.monitor;
super::app::run_on_main_async(move || {
super::with_main_state(|s| {
if let Some(o) = s.overlays.get(&monitor) {
o.window.orderFrontRegardless();
}
});
});
}
fn hide(&mut self) {
let monitor = self.monitor;
super::app::run_on_main_async(move || {
super::with_main_state(|s| {
if let Some(o) = s.overlays.get(&monitor) {
o.window.orderOut(None);
}
});
});
}
fn toggle(&mut self) {
if self.is_visible() {
self.hide();
} else {
self.show();
}
}
fn is_visible(&self) -> bool {
let monitor = self.monitor;
super::app::run_on_main_sync(move || {
super::with_main_state(|s| {
s.overlays
.get(&monitor)
.map(|o| o.window.isVisible())
.unwrap_or(false)
})
})
}
fn monitor(&self) -> MonitorId {
self.monitor
}
fn set_tint(&mut self, tint: Color) {
let monitor = self.monitor;
super::app::run_on_main_async(move || {
super::with_main_state(|s| {
if let Some(o) = s.overlays.get(&monitor) {
*o.view.ivars().tint.borrow_mut() = tint;
o.view.setNeedsDisplay(true);
}
});
});
}
fn set_input_capturing(&mut self, capturing: bool) {
let monitor = self.monitor;
super::app::run_on_main_async(move || {
let cursor_xy = if capturing { current_cursor_xy() } else { None };
super::with_main_state(|s| {
if let Some(o) = s.overlays.get(&monitor) {
o.window.setIgnoresMouseEvents(!capturing);
*o.view.ivars().captures_input.borrow_mut() = capturing;
if capturing {
let mtm = MainThreadMarker::new()
.expect("set_input_capturing on main thread");
let app = NSApplication::sharedApplication(mtm);
app.activateIgnoringOtherApps(true);
o.window.makeKeyAndOrderFront(None);
o.window.makeFirstResponder(Some(&o.view));
let was_hidden = *o.view.ivars().cursor_hidden.borrow();
if !was_hidden {
*o.view.ivars().cursor_hidden.borrow_mut() = true;
hide_cursor_on(&o.view);
unsafe { NSCursor::hide() };
}
} else {
let was_hidden = *o.view.ivars().cursor_hidden.borrow();
if was_hidden {
*o.view.ivars().cursor_hidden.borrow_mut() = false;
show_cursor_on(&o.view);
unsafe { NSCursor::unhide() };
}
}
}
});
if capturing {
if let (Some((sx, sy)), Some(tx)) = (cursor_xy, super::event_tx()) {
let (x, y) = screen_to_view_xy(monitor, sx, sy);
let _ = tx.send(PlatformEvent::PointerMove { monitor, x, y });
}
}
});
}
fn set_hud(&mut self, hud: Option<Hud>) {
let monitor = self.monitor;
super::app::run_on_main_async(move || {
super::with_main_state(|s| {
if let Some(o) = s.overlays.get(&monitor) {
use crate::hud_render::{
render_dynamic_into, render_static_into, static_hash,
};
let ivars = o.view.ivars();
let tint = match &hud {
Some(h) => h.background,
None => Color::TRANSPARENT,
};
*ivars.tint.borrow_mut() = tint;
let new_hash = hud.as_ref().map(static_hash).unwrap_or(0);
if new_hash != ivars.static_hud_hash.get() {
let image = hud
.as_ref()
.and_then(|h| rasterize_layer_for_view(&o.view, h, render_static_into));
*ivars.static_hud_image.borrow_mut() = image;
ivars.static_hud_hash.set(new_hash);
}
let dynamic = hud
.as_ref()
.and_then(|h| rasterize_layer_for_view(&o.view, h, render_dynamic_into));
*ivars.dynamic_hud_image.borrow_mut() = dynamic;
*ivars.hud.borrow_mut() = hud;
o.view.setNeedsDisplay(true);
}
});
});
}
fn set_background_frame(&mut self, frame: Option<crate::Frame>) {
let monitor = self.monitor;
super::app::run_on_main_async(move || {
let image = frame.and_then(|f| cgimage_from_rgba(&f.pixels, f.width, f.height));
super::with_main_state(|s| {
if let Some(o) = s.overlays.get(&monitor) {
*o.view.ivars().background_image.borrow_mut() = image;
o.view.setNeedsDisplay(true);
}
});
});
}
fn set_system_pointer_visible(&mut self, visible: bool) {
let monitor = self.monitor;
super::app::run_on_main_async(move || {
super::with_main_state(|s| {
if let Some(o) = s.overlays.get(&monitor) {
let was_hidden = *o.view.ivars().cursor_hidden.borrow();
if visible && was_hidden {
*o.view.ivars().cursor_hidden.borrow_mut() = false;
unsafe { NSCursor::unhide() };
show_cursor_on(&o.view);
} else if !visible && !was_hidden {
*o.view.ivars().cursor_hidden.borrow_mut() = true;
unsafe { NSCursor::hide() };
hide_cursor_on(&o.view);
}
}
});
});
}
fn set_pointing_hand_cursor(&mut self, pointing: bool) {
let monitor = self.monitor;
super::app::run_on_main_async(move || {
super::with_main_state(|s| {
if let Some(o) = s.overlays.get(&monitor) {
let prev = *o.view.ivars().pointing_hand.borrow();
if prev == pointing {
return;
}
*o.view.ivars().pointing_hand.borrow_mut() = pointing;
if !*o.view.ivars().cursor_hidden.borrow() {
show_cursor_on(&o.view);
}
}
});
});
}
fn confine_pointer(&mut self, _x: i32, _y: i32, _w: i32, _h: i32) {
}
fn release_pointer_confine(&mut self) {
}
}
pub(crate) struct OverlayIvars {
monitor: MonitorId,
tint: RefCell<Color>,
#[allow(dead_code)]
hud: RefCell<Option<Hud>>,
static_hud_image: RefCell<Option<CFRetained<CGImage>>>,
static_hud_hash: Cell<u64>,
dynamic_hud_image: RefCell<Option<CFRetained<CGImage>>>,
background_image: RefCell<Option<CFRetained<CGImage>>>,
cursor_hidden: RefCell<bool>,
pointing_hand: RefCell<bool>,
last_flags: RefCell<u64>,
transparent_cursor: RefCell<Option<Retained<objc2_app_kit::NSCursor>>>,
#[allow(dead_code)]
captures_input: RefCell<bool>,
}
define_class!(
#[unsafe(super(NSView))]
#[thread_kind = MainThreadOnly]
#[name = "VernierOverlayView"]
#[ivars = OverlayIvars]
pub(crate) struct OverlayView;
impl OverlayView {
#[unsafe(method(acceptsFirstResponder))]
fn accepts_first_responder(&self) -> bool {
true
}
#[unsafe(method(acceptsFirstMouse:))]
fn accepts_first_mouse(&self, _event: Option<&NSEvent>) -> bool {
true
}
#[unsafe(method(drawRect:))]
fn draw_rect(&self, _dirty: NSRect) {
let bounds = self.bounds();
let tint = *self.ivars().tint.borrow();
unsafe {
let color = NSColor::colorWithCalibratedRed_green_blue_alpha(
tint.r as f64 / 255.0,
tint.g as f64 / 255.0,
tint.b as f64 / 255.0,
tint.a as f64 / 255.0,
);
color.setFill();
objc2_app_kit::NSRectFill(bounds);
}
let ivars = self.ivars();
if let Some(image) = ivars.background_image.borrow().as_ref() {
draw_hud_image(bounds, image);
}
if let Some(image) = ivars.static_hud_image.borrow().as_ref() {
draw_hud_image(bounds, image);
}
if let Some(image) = ivars.dynamic_hud_image.borrow().as_ref() {
draw_hud_image(bounds, image);
}
}
#[unsafe(method(mouseMoved:))]
fn mouse_moved(&self, event: &NSEvent) {
forward_pointer_move(self, event);
}
#[unsafe(method(mouseDragged:))]
fn mouse_dragged(&self, event: &NSEvent) {
forward_pointer_move(self, event);
}
#[unsafe(method(mouseDown:))]
fn mouse_down(&self, event: &NSEvent) {
forward_pointer_button(self, event, BTN_LEFT, true);
}
#[unsafe(method(mouseUp:))]
fn mouse_up(&self, event: &NSEvent) {
forward_pointer_button(self, event, BTN_LEFT, false);
}
#[unsafe(method(rightMouseDown:))]
fn right_mouse_down(&self, event: &NSEvent) {
forward_pointer_button(self, event, BTN_RIGHT, true);
}
#[unsafe(method(rightMouseUp:))]
fn right_mouse_up(&self, event: &NSEvent) {
forward_pointer_button(self, event, BTN_RIGHT, false);
}
#[unsafe(method(keyDown:))]
fn key_down(&self, event: &NSEvent) {
forward_key(self, event, true);
}
#[unsafe(method(keyUp:))]
fn key_up(&self, event: &NSEvent) {
forward_key(self, event, false);
}
#[unsafe(method(flagsChanged:))]
fn flags_changed(&self, event: &NSEvent) {
forward_flags_changed(self, event);
}
#[unsafe(method(cursorUpdate:))]
fn cursor_update(&self, _event: &NSEvent) {
if *self.ivars().cursor_hidden.borrow() {
let cursor = transparent_cursor(self);
unsafe { cursor.set() };
}
}
#[unsafe(method(resetCursorRects))]
fn reset_cursor_rects(&self) {
if *self.ivars().cursor_hidden.borrow() {
let bounds = self.bounds();
let cursor = transparent_cursor(self);
unsafe { self.addCursorRect_cursor(bounds, &cursor) };
}
}
}
);
impl OverlayView {
fn new(mtm: MainThreadMarker, ivars: OverlayIvars) -> Retained<Self> {
let this = mtm.alloc::<Self>().set_ivars(ivars);
unsafe { msg_send![super(this), init] }
}
}
define_class!(
#[unsafe(super(NSWindow))]
#[thread_kind = MainThreadOnly]
#[name = "VernierOverlayWindow"]
pub(crate) struct OverlayWindow;
impl OverlayWindow {
#[unsafe(method(canBecomeKeyWindow))]
fn can_become_key_window(&self) -> bool {
true
}
#[unsafe(method(canBecomeMainWindow))]
fn can_become_main_window(&self) -> bool {
true
}
}
);
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
#[allow(dead_code)]
const BTN_MIDDLE: u32 = 0x112;
fn forward_pointer_move(view: &OverlayView, event: &NSEvent) {
let monitor = view.ivars().monitor;
let (x, y) = surface_local_point(view, event);
if let Some(tx) = super::event_tx() {
let _ = tx.send(PlatformEvent::PointerMove { monitor, x, y });
}
}
fn forward_pointer_button(view: &OverlayView, event: &NSEvent, button: u32, pressed: bool) {
let monitor = view.ivars().monitor;
let (x, y) = surface_local_point(view, event);
if let Some(tx) = super::event_tx() {
let _ = tx.send(PlatformEvent::PointerButton {
monitor,
button,
pressed,
x,
y,
});
}
}
const NS_FLAG_CAPS_LOCK: u64 = 1 << 16;
const NS_FLAG_SHIFT: u64 = 1 << 17;
const NS_FLAG_CONTROL: u64 = 1 << 18;
const NS_FLAG_OPTION: u64 = 1 << 19;
const NS_FLAG_COMMAND: u64 = 1 << 20;
fn forward_flags_changed(view: &OverlayView, event: &NSEvent) {
let flags: u64 = unsafe { event.modifierFlags() }.0 as u64;
let prev = *view.ivars().last_flags.borrow();
*view.ivars().last_flags.borrow_mut() = flags;
let monitor = view.ivars().monitor;
let Some(tx) = super::event_tx() else {
return;
};
for (mask, keysym) in [
(NS_FLAG_SHIFT, 0xFFE1u32),
(NS_FLAG_CONTROL, 0xFFE3),
(NS_FLAG_OPTION, 0xFFE9),
(NS_FLAG_COMMAND, 0xFFEB),
(NS_FLAG_CAPS_LOCK, 0xFFE5),
] {
let was = (prev & mask) != 0;
let now = (flags & mask) != 0;
if was != now {
let _ = tx.send(PlatformEvent::KeyboardKey {
monitor,
keysym,
pressed: now,
is_repeat: false,
});
}
}
}
fn forward_key(view: &OverlayView, event: &NSEvent, pressed: bool) {
let monitor = view.ivars().monitor;
let vkey = unsafe { event.keyCode() };
let is_repeat = pressed && unsafe { event.isARepeat() };
let keysym = vkey_to_xkb_keysym(vkey as u16);
if keysym == 0 {
return;
}
if let Some(tx) = super::event_tx() {
let _ = tx.send(PlatformEvent::KeyboardKey {
monitor,
keysym,
pressed,
is_repeat,
});
}
}
fn current_cursor_xy() -> Option<(f64, f64)> {
let p = unsafe { objc2_app_kit::NSEvent::mouseLocation() };
Some((p.x, p.y))
}
fn screen_to_view_xy(monitor: MonitorId, sx: f64, sy: f64) -> (f64, f64) {
let height = super::with_main_state(|s| {
s.overlays
.get(&monitor)
.map(|o| o.window.frame())
.map(|f| f.size.height)
.unwrap_or(0.0)
});
let frame_origin = super::with_main_state(|s| {
s.overlays
.get(&monitor)
.map(|o| o.window.frame().origin)
.unwrap_or(NSPoint { x: 0.0, y: 0.0 })
});
let local_x = sx - frame_origin.x;
let local_y_bottom_up = sy - frame_origin.y;
(local_x, height - local_y_bottom_up)
}
fn install_tracking_area(view: &OverlayView, size: objc2_foundation::NSSize) {
use objc2_app_kit::{NSTrackingArea, NSTrackingAreaOptions};
use objc2::runtime::AnyObject;
let rect = NSRect {
origin: NSPoint { x: 0.0, y: 0.0 },
size,
};
let options = NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::CursorUpdate
| NSTrackingAreaOptions::ActiveAlways
| NSTrackingAreaOptions::InVisibleRect;
let owner: &AnyObject = unsafe { &*((&**view) as *const NSView as *const AnyObject) };
let area: Retained<NSTrackingArea> = unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
rect,
options,
Some(owner),
None,
)
};
view.addTrackingArea(&area);
}
fn surface_local_point(view: &OverlayView, event: &NSEvent) -> (f64, f64) {
let p_window = unsafe { event.locationInWindow() };
let p_view = view.convertPoint_fromView(p_window, None);
let height = view.bounds().size.height;
(p_view.x, height - p_view.y)
}
fn hide_cursor_on(view: &OverlayView) {
if let Some(window) = view.window() {
unsafe { window.invalidateCursorRectsForView(view) };
}
let cursor = transparent_cursor(view);
unsafe { cursor.set() };
}
fn show_cursor_on(view: &OverlayView) {
use objc2_app_kit::NSCursor;
if let Some(window) = view.window() {
unsafe { window.invalidateCursorRectsForView(view) };
}
let pointing = *view.ivars().pointing_hand.borrow();
let cursor = if pointing {
unsafe { NSCursor::pointingHandCursor() }
} else {
unsafe { NSCursor::arrowCursor() }
};
unsafe { cursor.set() };
}
fn transparent_cursor(view: &OverlayView) -> Retained<objc2_app_kit::NSCursor> {
if let Some(c) = view.ivars().transparent_cursor.borrow().as_ref() {
return c.clone();
}
use objc2_app_kit::{NSCursor, NSImage};
use objc2_foundation::NSSize;
let size = NSSize {
width: 16.0,
height: 16.0,
};
let image = unsafe { NSImage::initWithSize(NSImage::alloc(), size) };
let hot = NSPoint { x: 0.0, y: 0.0 };
let cursor = unsafe {
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hot)
};
*view.ivars().transparent_cursor.borrow_mut() = Some(cursor.clone());
cursor
}
fn rasterize_layer_for_view(
view: &OverlayView,
hud: &Hud,
render_fn: fn(&mut [u8], u32, u32, u32, &Hud),
) -> Option<CFRetained<CGImage>> {
let bounds = view.bounds();
let scale = view
.window()
.map(|w| w.backingScaleFactor())
.unwrap_or(1.0)
.max(1.0);
let phys_w = ((bounds.size.width * scale).round() as u32).max(1);
let phys_h = ((bounds.size.height * scale).round() as u32).max(1);
let mut canvas = vec![0u8; (phys_w as usize) * (phys_h as usize) * 4];
render_fn(
&mut canvas,
phys_w,
phys_h,
scale.round().max(1.0) as u32,
hud,
);
cgimage_from_rgba(&canvas, phys_w, phys_h)
}
fn cgimage_from_rgba(
rgba: &[u8],
width: u32,
height: u32,
) -> Option<objc2_core_foundation::CFRetained<objc2_core_graphics::CGImage>> {
use objc2_core_foundation::CFData;
use objc2_core_graphics::{
CGBitmapInfo, CGColorRenderingIntent, CGColorSpace, CGDataProvider, CGImage,
CGImageAlphaInfo,
};
let len = rgba.len() as isize;
let data = unsafe { CFData::new(None, rgba.as_ptr(), len) }?;
let provider = CGDataProvider::with_cf_data(Some(&data))?;
let colorspace = CGColorSpace::new_device_rgb()?;
let bitmap_info = CGBitmapInfo(CGImageAlphaInfo::PremultipliedLast.0);
unsafe {
CGImage::new(
width as usize,
height as usize,
8,
32,
(width as usize) * 4,
Some(&colorspace),
bitmap_info,
Some(&provider),
std::ptr::null(),
false,
CGColorRenderingIntent::RenderingIntentDefault,
)
}
}
fn draw_hud_image(bounds: NSRect, image: &CFRetained<CGImage>) {
use objc2_app_kit::NSGraphicsContext;
use objc2_core_foundation::CGRect as CFRect;
let Some(ctx) = (unsafe { NSGraphicsContext::currentContext() }) else {
return;
};
let cg_ctx = unsafe { ctx.CGContext() };
let rect = CFRect {
origin: objc2_core_foundation::CGPoint { x: 0.0, y: 0.0 },
size: objc2_core_foundation::CGSize {
width: bounds.size.width,
height: bounds.size.height,
},
};
unsafe {
objc2_core_graphics::CGContext::draw_image(Some(&cg_ctx), rect, Some(image));
}
}