Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added MacOS menu #1583

Merged
merged 9 commits into from
Apr 24, 2021
Merged

Conversation

casperstorm
Copy link
Contributor

@casperstorm casperstorm commented May 28, 2020

This is an WIP PR which shows a way we could add a generic MacOS menu into winit.
Let me know what you think, if this is something you could see in this repo, then we could collaborate about the details, formats, where to initialize the menu etc.

Screenshot 2020-05-28 at 19 41 00

  • Tested on all platforms changed
  • Compilation warnings were addressed
  • cargo fmt has been run on this branch
  • cargo doc builds successfully
  • Added an entry to CHANGELOG.md if knowledge of this change could be valuable to users
  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior
  • Created or updated an example program if it would help users understand this functionality
  • Updated feature matrix, if new features were added or implemented

Fixes #41.

@casperstorm
Copy link
Contributor Author

@kchibisov, you mentioned you had some ideas here. Do you mind sharing them here?

@adamnemecek
Copy link

Cool feature. Hope this gets merged!

@OneSadCookie
Copy link

+1 for this; I won't use winit because winit apps don't behave anything like a standard Mac app, and this is a big step forward.

This is very English-centric, should you perhaps call NSLocalizedString (or if winit has a blessed localization solution, possibly use that) for the menu & item titles?

@OneSadCookie
Copy link

This has been open six months now, what do we need to do to get it merged?

@kchibisov
Copy link
Member

This has been open six months now, what do we need to do to get it merged?

I haven't seen macOS maintainers in winit for similar amount of time, so someone with macOS should review it. I have macOS machine now, will try to look into it tomorrow, but I'm not a macOS maintainer.

@kchibisov kchibisov self-requested a review November 21, 2020 19:36
@casperstorm
Copy link
Contributor Author

I found a error myself where the application is not interactable initially launched. If you switch app, and then back then you can interacte with the menu. Druid has fixed it in this commit: linebender/druid@05311d1#diff-5d6b8d2cc218bec3a8ee3ef9c4c47c61

We should handle this in this PR before we merge.

@adamnemecek
Copy link

Also, how exactly can my app observe that a menu item has been clicked? Sorry if this is an obvious question.

@adamnemecek
Copy link

adamnemecek commented Nov 21, 2020

I think that we need to think about the API. The way it works right now is that NSMenuItem has a selector and target properties and when the menu item is clicked, the system invokes selector on target. This seems kind of terrible for Rust since this would force me to deal implementing objective-c methods on my Rust structs.

I think that a better API would be adding a new WindowEvent case (compiled optionally for macos), called something like MenuItem which will allow the user to handle menu item events just like any other event when they are handling winit events.

The way to do this is adding a new struct

pub struct MenuItem {
     name: String,
     ...
}

and then add some APIs to Window to manage (add, delete) menu items. When you register a new menu item, winit dynamically adds a new method (named the same as the menu item) to the application delegate. When the application delegate receives the message that the item has been clicked, it simply injects a menu item event into the event stream.

@kchibisov
Copy link
Member

I think that we need to think about the API. The way it works right now is that NSMenuItem has a selector and target properties and when the menu item is clicked, the system invokes selector on target. This seems kind of terrible for Rust since this would force me to deal implementing objective-c methods on my Rust structs.

The thing is that I'm not sure that there should be any special handling for them, since the handling is always the same for everyone? Like if you do HideOtherWindows, the only way to handle is either ignore or call winit method to hide other windows, since the amount of menu items is trivial.

I'm not sure about providing custom API right now, since it seems like a very complicated approach and requiring user to match arbitrary strings isn't rust API either.

Right now I think providing a common menu handler for everything(that's what glfw does for example) seems like a reasonable enough solution.

@adamnemecek
Copy link

@kchibisov I don't follow. When I register these menu items and one gets clicked, how does is that information delivered to my application?

@kchibisov
Copy link
Member

@kchibisov I don't follow. When I register these menu items and one gets clicked, how does is that information delivered to my application?

With the current API it doesn't, macOS just calls a selector you've specified, and I'm not sure if you can generate selectors on the fly, especially if we talk about rust side, however I'm not familiar with macOS API.

@casperstorm do you aware if there's a way to provide a custom selector and identify in that selector from which method you've been called, thus all menu items can share a selector?

@adamnemecek
Copy link

adamnemecek commented Nov 21, 2020

@kchibisov there is not a simple way to provide a method that would be called by all menu items. You can always do something like forwardInvocation https://stackoverflow.com/questions/9800293/equivalent-of-ruby-method-missing-in-objective-c-ios

however then you have to know if the method you have been passed is a method from menu item.

@adamnemecek
Copy link

@kchibisov but yes, you can generate selectors on the fly with NSSelectorFromString. The trick is that we would be doing this only on the objective c side, on the rust side, we would just inject a MenuItem event into the event stream.

@adamnemecek
Copy link

adamnemecek commented Nov 21, 2020

Another way would be to use the NSMenuItem tag property to allow all menu items to share the same selector. This approach might make more sense since you don't have to register the selectors dynamically.

We could then have a

struct MenuItem<T: From<u64> + Into<u64>> {
     tag: T,
     title: String,
}

you would then have to implement an enum that's convertible to u64.

winit would then insert a NSMenuItem with selector set to "handleMenuItem" and with the tag set to tag converted to u64.

Then, in the handler, we would convert the tag back into T and pass it to the user.

I think this is as good as it gets to be honest.

@OneSadCookie
Copy link

OneSadCookie commented Nov 21, 2020

I think it's worth distinguishing between "winit needs working menus to act like a standard mac app" and "winit should provide a general menu interface". The latter is a huge problem, with implications for Windows and Linux as well. The former only implies handling a few standard commands in standard ways.

I don't think we should gate this PR on developing a general cross-platform menu interface.

About, Hide/Show, Quit, can be handled in the standard ways without modifying winit's APIs or sending new events to the user. The other items can be disabled if they can't be omitted.

@adamnemecek
Copy link

It doesn't need to be general, it can be conditionally compiled for now, but ideally I should be able to use winit to add new menu items right?

@OneSadCookie
Copy link

OneSadCookie commented Nov 21, 2020

Speaking for myself (but also as someone used to consuming GLFW, SDL, etc) I really don't care about custom menu items. I care about "clicking the menubar shows a menu" and "command-Q works as expected and by default".

Not saying winit shouldn't get a customizable menu system in the future, I just think that this is a critical step forward that doesn't require it, and that will be delayed by trying to get that right.

@kchibisov
Copy link
Member

@adamncasey if you have a solution(code) to a global problem of menu items for macOS, I'm all ears, but I don't care about custom menu items, especially if you really want them you can add some in downstream without many issues.

@OneSadCookie
Copy link

What this PR delivers already, is all that GLFW and SDL deliver. Parity there is a great first step, and doesn't preclude doing something better in future.

@kchibisov
Copy link
Member

What this PR delivers already, is all that GLFW and SDL deliver. Parity there is a great first step, and doesn't preclude doing something better in future.

That's my point as well, since it does precisely what glfw does, and winit is kind of on par with glfw in what it should do (maybe except some minor things like providing OpenGL/Vulkan surface, but that's it).

@adamnemecek
Copy link

adamnemecek commented Nov 22, 2020

Btw instead of let _pool = NSAutoreleasePool::new(nil); you can use the autoreleasepool function. I think that a result, you don't need to be call autorelease. Check out this example gfx-rs/metal-rs@ae3733c#diff-d1b0398de8154e02b9ab104eb1b0206139165ec972a8b62a0378ffc367db79a9L16

@adamnemecek
Copy link

Also, there's this project called stella2 that does cross platform menu handling https://github.com/yvt/Stella2/blob/c0524f1a18ad4ee72460a0db0d2bdb9fcb48e3a2/stella2/src/view/global.rs#L16

@LoganDark
Copy link
Contributor

I think this PR should be merged as-is. Allowing for custom menus can be done later but this is something that is needed now.

@casperstorm
Copy link
Contributor Author

@kchibisov let me know if there's anything I need to do here.

@LoganDark
Copy link
Contributor

LoganDark commented Feb 26, 2021

@kchibisov let me know if there's anything I need to do here.

I think they're waiting on i18n of the menu text. You may as well do that anyway because it's important

Copy link
Member

@madsmtm madsmtm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I welcome this addition, I think it's something that winit should definitely support out of the box!

However, I'm working on a library myself for supporting this, and for that it would be really nice this was an optional feature (either compilation or a setting on the event loop), so winit and my custom library don't both try to set the main menu!

Haven't looked at NSAutoreleasePool in this review, don't know much about it.

src/platform_impl/macos/mod.rs Outdated Show resolved Hide resolved
src/platform_impl/macos/menu.rs Outdated Show resolved Hide resolved
src/platform_impl/macos/menu.rs Outdated Show resolved Hide resolved
src/platform_impl/macos/menu.rs Outdated Show resolved Hide resolved

// Seperator menu item
let sep_first = NSMenuItem::separatorItem(nil).autorelease();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the Services menu here (can be done by creating a menu item with an empty menu, and registering that as the servicesMenu)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, not sure it make sense if we don't have any services to add?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into how the Services menu is used. Winit uses a custom NSView subclass for the entire window surface. Applications and custom NSView types have to implement a certain protocol (see "Using Services" in the Apple Developer Documentation Archive). More specifically the NSView has to provide an implementation for validRequestorForSendType through which it must specify whether the currently selected thing is a valid target for a service that operates on the provided types of data. However winit has no idea what is selected currently. Given that winit doesn't provide APIs to handle validRequestorForSendType messages, it's currently not possible to use the Services menu through winit (if I understand correctly). This is somewhat related to #1759 in that it's a macOS specific "event" that a higher level code may want to be able to handle.

Anyhow I don't think we should be concerned about that for this PR. So with all that, just like @casperstorm wrote, it seems there's no point in adding the Services menu.

@LoganDark
Copy link
Contributor

so winit and my custom library don't both try to set the main menu!

Can't you grab the main menu after the fact and modify it?

@madsmtm
Copy link
Member

madsmtm commented Feb 28, 2021

Can't you grab the main menu after the fact and modify it?

Easily, but would like to avoid adding creating a menu twice, but mostly it's because I'm not sure if changing the main menu after it's been assigned would introduce flicker

@LoganDark
Copy link
Contributor

Can't you grab the main menu after the fact and modify it?

Easily, but would like to avoid adding creating a menu twice, but mostly it's because I'm not sure if changing the main menu after it's been assigned would introduce flicker

You won't have to create a second menu if a menu is already assigned. Pretty standard "only create if it doesn't already exist" pattern.

@madsmtm
Copy link
Member

madsmtm commented Feb 28, 2021

Sure, I could just support that usecase in my library (which it probably should anyhow), and the flicker, if even present, is probably not that big of a deal; it was just a nicety for me, as a user, to have 😉

@LoganDark
Copy link
Contributor

Sure, I could just support that usecase in my library (which it probably should anyhow), and the flicker, if even present, is probably not that big of a deal; it was just a nicety for me, as a user, to have 😉

It's not uncommon for an application to launch without a menu bar and then take seconds to fill in the categories. Seriously, if your library works fast enough for the delay to be called nothing more than a flicker, I'll be impressed.

@ArturKovacs
Copy link
Contributor

As far as I understand NSLocalizedString doesn't help with localizing this. First of all NSLocalizedString is a C macro and it doesn't seem to be available in rust. According to the docs, the macro returns what localizedStringForKey:value:table: would return on the mainBundle with the table set to nil but as far as I can tell this doesn't magically translate words. It only translates the things that the developer translated themselves and added to the bundle.

My knowledge is very limited on this and for the most part I don't know what I'm talking about so please correct me if I'm wrong. If I'm right however, then I don't think this PR should be concerned with localization.

@ArturKovacs
Copy link
Contributor

ArturKovacs commented Apr 13, 2021

@casperstorm if you address @madsmtm's comments then I think I would be happy to merge this PR.

@casperstorm
Copy link
Contributor Author

casperstorm commented Apr 23, 2021

@casperstorm if you address @madsmtm's comments then I think I would be happy to merge this PR.

Thanks for the suggestions everyone. I have updated, and fixed a few things. There's still some outstandings I would like some help and feedback to solve.

@ArturKovacs
Copy link
Contributor

@casperstorm I added a commit to this PR, please pull and test to see if it works as expected. If you are fine with the changes I made, I think we can merge this.

@casperstorm
Copy link
Contributor Author

@casperstorm I added a commit to this PR, please pull and test to see if it works as expected. If you are fine with the changes I made, I think we can merge this.

Thanks, @ArturKovacs. It works as expected here.

@ArturKovacs
Copy link
Contributor

Shoot! I completely forgot about the activation policy thing. I removed the line that set the activation policy and it still seems to work fine for me. What was the reason you originally put it there @casperstorm? Does my latest commit still work for you?

@casperstorm
Copy link
Contributor Author

casperstorm commented Apr 24, 2021

Shoot! I completely forgot about the activation policy thing. I removed the line that set the activation policy and it still seems to work fine for me. What was the reason you originally put it there @casperstorm? Does my latest commit still work for you?

Last commit works for me. Honestly I can't remember why I put it there. It's been quite awhile :)

@ArturKovacs ArturKovacs merged commit e8cdf8b into rust-windowing:master Apr 24, 2021
@ArturKovacs
Copy link
Contributor

Thanks for the contribution!

@OneSadCookie
Copy link

Thank you @casperstorm for doing the work and @ArturKovacs for getting it merged! Been waiting a long time for this one :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Command Q to quit on Mac OS X
8 participants