kael 0.2.0

GPU-accelerated native UI framework for Rust — build desktop apps with Metal, DirectX, and Vulkan rendering
Documentation
use crate::platform::TrayMenuItem;
use crate::{Bounds, Pixels, point, px, size};
use objc2::msg_send;
use objc2::runtime::{AnyClass, AnyObject, Sel};
use objc2_foundation::{NSRect, NSSize, NSString};
use std::cell::Cell;
use std::ffi::c_void;

#[allow(non_camel_case_types)]
type id = *mut AnyObject;
#[allow(non_upper_case_globals)]
const nil: id = std::ptr::null_mut();

pub(crate) struct MacTray {
    status_item: *mut AnyObject,
    panel_mode: Cell<bool>,
    stored_menu: Cell<*mut AnyObject>,
}

unsafe fn class(name: &std::ffi::CStr) -> &'static AnyClass {
    AnyClass::get(name).unwrap_or_else(|| panic!("missing class {name:?}"))
}

impl MacTray {
    pub fn new() -> Self {
        unsafe {
            let status_bar: *mut AnyObject = msg_send![class(c"NSStatusBar"), systemStatusBar];
            let length: f64 = -1.0;
            let status_item: *mut AnyObject = msg_send![status_bar, statusItemWithLength: length];
            let _: *mut AnyObject = msg_send![status_item, retain];
            let _: () = msg_send![status_item, setVisible: true];

            let button: *mut AnyObject = msg_send![status_item, button];
            if !button.is_null() {
                let default_title = NSString::from_str("App");
                let _: () = msg_send![button, setTitle: &*default_title];
            }

            Self {
                status_item,
                panel_mode: Cell::new(false),
                stored_menu: Cell::new(std::ptr::null_mut()),
            }
        }
    }

    pub fn set_icon(&self, icon_data: Option<&[u8]>) {
        unsafe {
            let button: *mut AnyObject = msg_send![self.status_item, button];
            if button.is_null() {
                return;
            }
            match icon_data {
                Some(data) => {
                    let ns_data: *mut AnyObject = msg_send![
                        class(c"NSData"),
                        dataWithBytes: data.as_ptr() as *const c_void,
                        length: data.len() as u64
                    ];
                    let image_alloc: *mut AnyObject = msg_send![class(c"NSImage"), alloc];
                    let image: *mut AnyObject = msg_send![image_alloc, initWithData: ns_data];
                    if !image.is_null() {
                        let target_size = NSSize::new(18.0, 18.0);
                        let _: () = msg_send![image, setSize: target_size];
                        let _: () = msg_send![image, setTemplate: true];
                        let _: () = msg_send![button, setImage: image];
                        let empty = NSString::from_str("");
                        let _: () = msg_send![button, setTitle: &*empty];
                    }
                }
                None => {
                    let _: () = msg_send![button, setImage: std::ptr::null_mut::<AnyObject>()];
                }
            }
        }
    }

    #[allow(dead_code)]
    pub fn set_title(&self, title: &str) {
        unsafe {
            let button: *mut AnyObject = msg_send![self.status_item, button];
            if button.is_null() {
                return;
            }
            let ns_title = NSString::from_str(title);
            let _: () = msg_send![button, setTitle: &*ns_title];
        }
    }

    pub fn set_tooltip(&self, tooltip: &str) {
        unsafe {
            let button: *mut AnyObject = msg_send![self.status_item, button];
            if button.is_null() {
                return;
            }
            let ns_tooltip = NSString::from_str(tooltip);
            let _: () = msg_send![button, setToolTip: &*ns_tooltip];
        }
    }

    pub fn set_menu(&self, items: Vec<TrayMenuItem>) {
        unsafe {
            let old_menu = self.stored_menu.get();
            if !old_menu.is_null() {
                let _: () = msg_send![old_menu, release];
            }

            let menu: *mut AnyObject = msg_send![class(c"NSMenu"), new];
            let _: () = msg_send![menu, setAutoenablesItems: false];
            let selector = Sel::register(c"handleTrayMenuItem:");
            build_menu_with_selector(menu as id, &items, selector);

            self.stored_menu.set(menu);

            if !self.panel_mode.get() {
                let _: () = msg_send![self.status_item, setMenu: menu];
            }
        }
    }

    pub fn set_panel_mode(&self, enabled: bool) {
        self.panel_mode.set(enabled);
        unsafe {
            if enabled {
                let _: () = msg_send![self.status_item, setMenu: std::ptr::null_mut::<AnyObject>()];

                let button: *mut AnyObject = msg_send![self.status_item, button];
                if !button.is_null() {
                    let delegate = get_app_delegate();
                    if !delegate.is_null() {
                        let _: () = msg_send![button, setTarget: delegate];
                        let panel_sel = Sel::register(c"handleTrayPanelClick:");
                        let _: () = msg_send![button, setAction: panel_sel];
                    }
                }
            } else {
                let button: *mut AnyObject = msg_send![self.status_item, button];
                if !button.is_null() {
                    let _: () = msg_send![button, setTarget: std::ptr::null_mut::<AnyObject>()];
                    let null_sel: Option<Sel> = None;
                    let _: () = msg_send![button, setAction: null_sel];
                }

                let stored = self.stored_menu.get();
                if !stored.is_null() {
                    let _: () = msg_send![self.status_item, setMenu: stored];
                }
            }
        }
    }

    pub fn get_icon_bounds(&self) -> Option<Bounds<Pixels>> {
        unsafe {
            let button: *mut AnyObject = msg_send![self.status_item, button];
            if button.is_null() {
                return None;
            }

            let button_window: *mut AnyObject = msg_send![button, window];
            if button_window.is_null() {
                return None;
            }

            let frame: NSRect = msg_send![button_window, frame];

            let main_screen: *mut AnyObject = msg_send![class(c"NSScreen"), mainScreen];
            if main_screen.is_null() {
                return None;
            }
            let screen_frame: NSRect = msg_send![main_screen, frame];

            let flipped_y = screen_frame.size.height - frame.origin.y - frame.size.height;

            Some(Bounds::new(
                point(px(frame.origin.x as f32), px(flipped_y as f32)),
                size(px(frame.size.width as f32), px(frame.size.height as f32)),
            ))
        }
    }
}

impl Drop for MacTray {
    fn drop(&mut self) {
        unsafe {
            let stored = self.stored_menu.get();
            if !stored.is_null() {
                let _: () = msg_send![stored, release];
            }
            let status_bar: *mut AnyObject = msg_send![class(c"NSStatusBar"), systemStatusBar];
            let _: () = msg_send![status_bar, removeStatusItem: self.status_item];
            let _: () = msg_send![self.status_item, release];
        }
    }
}

unsafe fn get_app_delegate() -> *mut AnyObject {
    unsafe {
        let app: *mut AnyObject = msg_send![class(c"NSApplication"), sharedApplication];
        msg_send![app, delegate]
    }
}

pub(crate) unsafe fn configure_actionable_item_with_selector(
    menu_item: id,
    item_id: &str,
    selector: Sel,
) {
    unsafe {
        let delegate = get_app_delegate();
        if !delegate.is_null() {
            let menu_item = menu_item as *mut AnyObject;
            let _: () = msg_send![menu_item, setTarget: delegate];
            let _: () = msg_send![menu_item, setAction: selector];
            let represented = NSString::from_str(item_id);
            let _: () = msg_send![menu_item, setRepresentedObject: &*represented];
            let _: () = msg_send![menu_item, setEnabled: true];
        }
    }
}

pub(crate) unsafe fn build_menu_with_selector(menu: id, items: &[TrayMenuItem], selector: Sel) {
    unsafe {
        let menu = menu as *mut AnyObject;
        for item in items {
            match item {
                TrayMenuItem::Action { label, id } => {
                    let title = NSString::from_str(label.as_ref());
                    let menu_item_alloc: *mut AnyObject = msg_send![class(c"NSMenuItem"), alloc];
                    let empty = NSString::from_str("");
                    let null_sel: Option<Sel> = None;
                    let menu_item: *mut AnyObject = msg_send![
                        menu_item_alloc,
                        initWithTitle: &*title,
                        action: null_sel,
                        keyEquivalent: &*empty
                    ];
                    configure_actionable_item_with_selector(menu_item as id, id.as_ref(), selector);
                    let _: () = msg_send![menu, addItem: menu_item];
                }
                TrayMenuItem::Separator => {
                    let separator: *mut AnyObject = msg_send![class(c"NSMenuItem"), separatorItem];
                    let _: () = msg_send![menu, addItem: separator];
                }
                TrayMenuItem::Submenu {
                    label,
                    items: sub_items,
                } => {
                    let title = NSString::from_str(label.as_ref());
                    let menu_item_alloc: *mut AnyObject = msg_send![class(c"NSMenuItem"), alloc];
                    let empty = NSString::from_str("");
                    let null_sel: Option<Sel> = None;
                    let menu_item: *mut AnyObject = msg_send![
                        menu_item_alloc,
                        initWithTitle: &*title,
                        action: null_sel,
                        keyEquivalent: &*empty
                    ];
                    let submenu: *mut AnyObject = msg_send![class(c"NSMenu"), new];
                    build_menu_with_selector(submenu as id, sub_items, selector);
                    let _: () = msg_send![menu_item, setSubmenu: submenu];
                    let _: () = msg_send![menu, addItem: menu_item];
                }
                TrayMenuItem::Toggle { label, checked, id } => {
                    let title = NSString::from_str(label.as_ref());
                    let menu_item_alloc: *mut AnyObject = msg_send![class(c"NSMenuItem"), alloc];
                    let empty = NSString::from_str("");
                    let null_sel: Option<Sel> = None;
                    let menu_item: *mut AnyObject = msg_send![
                        menu_item_alloc,
                        initWithTitle: &*title,
                        action: null_sel,
                        keyEquivalent: &*empty
                    ];
                    configure_actionable_item_with_selector(menu_item as id, id.as_ref(), selector);
                    let state: isize = if *checked { 1 } else { 0 };
                    let _: () = msg_send![menu_item, setState: state];
                    let _: () = msg_send![menu, addItem: menu_item];
                }
            }
        }
        let _ = nil;
    }
}