madsmtm / objc2

Bindings to Apple's frameworks in Rust
https://docs.rs/objc2/
MIT License
375 stars 41 forks source link

How to pass a Rust function as a selector? #614

Open ghostman2013 opened 6 months ago

ghostman2013 commented 6 months ago

Hello!

I use a little modified code that's based on examples from objc2-app-kit crate sources:

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);
        }
    }
}

Just a trivial AppDelegate with menu builder. However, I hit troubles at this step:

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

The on_click: fn() is a typical Rust function but I can't get how to pass it as a selector, to NSMenuItem called it on click.

Unfortunately, I couldn't find any examples. Could you explain, please, how I can make it?

madsmtm commented 6 months ago

This is a common pattern used in Apple's frameworks, see the documentation on Target-Action, and it's a bit of a pain to handle when you're used to Rust's closures.

There's a similar issue here: https://github.com/madsmtm/objc2/issues/585

Try to have a look at that, in short you have to make a method on your delegate with a selector like onClick:, place it on the menu item with .setAction(Some(sel!(onClick:))) and .setTarget(Some(self)), and then you have to run your click handler in onClick:.

I know that's not a real explanation, but I don't have time to write up a full example right now, will do so later.

ghostman2013 commented 6 months ago

This is a common pattern used in Apple's frameworks, see the documentation on Target-Action, and it's a bit of a pain to handle when you're used to Rust's closures.

There's a similar issue here: #585

Try to have a look at that, in short you have to make a method on your delegate with a selector like onClick:, place it on the menu item with .setAction(Some(sel!(onClick:))) and .setTarget(Some(self)), and then you have to run your click handler in onClick:.

I know that's not a real explanation, but I don't have time to write up a full example right now, will do so later.

No, you have explained it very clearly. Thank you! I got how to make.

Korne127 commented 4 months ago

Since you said in https://github.com/rust-windowing/winit/issues/1751 that the object containing the selector methods doesn't need to be an NSApplicationDelegate, I tried following this with a custom class like this

pub fn create_menu() {
    let mtm = MainThreadMarker::new().unwrap();
    let menu_bar_handler = MenuBarHandler::new(mtm);

    let app = NSApplication::sharedApplication(mtm);
    let menu_bar = NSMenu::new(mtm);
    app.setMainMenu(Some(&menu_bar));

    let app_menu_header = NSMenuItem::new(mtm);
    let app_menu = NSMenu::new(mtm);

    let quit_item = unsafe {
        NSMenuItem::initWithTitle_action_keyEquivalent(
            mtm.alloc(),
            ns_string!("Test"),
            Some(sel!(testaction:)),
            ns_string!("t"),
        )
    };
    unsafe { quit_item.setTarget(Some(&menu_bar_handler)) };

    app_menu.addItem(&quit_item);
    app_menu_header.setSubmenu(Some(&app_menu));
    menu_bar.addItem(&app_menu_header);
}

declare_class!(
    struct MenuBarHandler;

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

    impl DeclaredClass for MenuBarHandler {}

    unsafe impl MenuBarHandler {
        #[method(testaction:)]
        fn open(&self, _: &NSNotification) {
            println!("This is a test");
        }
    }
);

impl MenuBarHandler {
    fn new(mtm: MainThreadMarker) -> Retained<Self> {
        unsafe { msg_send_id![mtm.alloc(), init] }
    }
}

However, while the menu bar appears as expected, the Test item is grated out, which means that the selector isn't linked correctly to the method. Can you tell me what I'm doing wrong?

madsmtm commented 4 months ago

See the explanation in https://github.com/madsmtm/objc2/issues/585#issuecomment-1965482253, the issue is that you don't store menu_bar_handler anywhere, so it's deallocated at the end of create_menu.

Korne127 commented 4 months ago

Thank you for the reply! Yes, that was the issue. I guess it's not bad to have an (otherwise) working example here in case other people stumble on this too though.