Denis Steinman
Denis Steinman

Reputation: 7799

How to pass a Rust function as a selector into NSMenuItem.action using objc2?

I have the next code to build an application menu in AppDelegate (an AppDelegate code was taken from the official example):

impl AppDelegate {
    fn new(mtm: MainThreadMarker, menu: Option<Menu>) -> Id<Self> {
        let this = mtm.alloc();
        let this = this.set_ivars(Ivars {
            mtm,
            menu,
        });
        unsafe { msg_send_id![super(this), init] }
    }

    fn build_menu(&self, menu: &Menu, ns_menu: &Id<NSMenu>) {
        let ivars = self.ivars();
        for item in menu.items.iter() {
            let ns_menu_item = NSMenuItem::new(ivars.mtm);
            let title = NSString::from_str(&item.name);
            unsafe { ns_menu_item.setTitle(&title) };
            
            if let Some(on_click) = &item.on_click {
                unsafe { ns_menu_item.setAction(Some(on_click)) };
            }

            if let Some(submenu) = &item.submenu {
                let ns_submenu = NSMenu::new(ivars.mtm);
                self.build_menu(submenu, &ns_submenu);
                ns_menu_item.setSubmenu(Some(&ns_submenu));
            }
            
            ns_menu.addItem(&ns_menu_item);
        }
    }

    fn create_menu(&self, application: Id<NSApplication>) {
        let ivars = self.ivars();
        if let Some(menu) = &ivars.menu {
            let main_menu = NSMenu::new(ivars.mtm);
            application.setMainMenu(Some(&main_menu));

            self.build_menu(menu, &main_menu);
        }
    }
}

The problem is in this part of the code:

if let Some(on_click) = &item.on_click {
    unsafe { ns_menu_item.setAction(Some(on_click)) };
}

The on_click: fn() is a Rust function and I can't get how to pass it as an Objective-C selector using objc2.

In generated AppKit that method looks as below:

#[method(setAction:)]
pub unsafe fn setAction(&self, action: Option<Sel>);

Upvotes: 0

Views: 110

Answers (1)

Denis Steinman
Denis Steinman

Reputation: 7799

I'm pretty sure there's a much simpler way but... I've resolved my problem using a custom NSMenuItem class:

#[derive(Debug)]
#[allow(unused)]
struct MacMenuItemIvars {
    action: Option<fn()>,
}

declare_class!(
    struct MacMenuItem;

    unsafe impl ClassType for MacMenuItem {
        type Super = NSMenuItem;
        type Mutability = mutability::MainThreadOnly;
        const NAME: &'static str = "MenuItem";
    }

    impl DeclaredClass for MacMenuItem {
        type Ivars = MacMenuItemIvars;
    }

    unsafe impl MacMenuItem {
        #[method(callback)]
        fn __callback(&self) {
            if let Some(action) = self.ivars().action {
                action();
            }
        }
    }

    unsafe impl NSObjectProtocol for MacMenuItem {}
);

impl MacMenuItem {
    fn new<S>(mtm: MainThreadMarker, title: S, action: Option<fn()>) -> Id<Self>
    where
        S: Into<String>,
    {
        let this = mtm.alloc();
        let this = this.set_ivars(MacMenuItemIvars {
            action,
        });
        let item: Id<MacMenuItem> = unsafe { msg_send_id![super(this), init] };

        let title: String = title.into();
        let title = NSString::from_str(&title);
        unsafe { item.setTitle(&title) };

        if action.is_some() {
            unsafe { item.setTarget(Some(&item)) };
            unsafe { item.setAction(Some(sel!(callback))) };
        }

        item
    }
}

There must be a way to achieve the same result using Objective-C blocks. However, I didn't get how.

Upvotes: 0

Related Questions