1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
//! A wrapper for NSMenuItem. Currently only supports menus going
//! one level deep; this could change in the future but is fine for
//! now.

use std::fmt;
use std::sync::Once;

use block::ConcreteBlock;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use objc_id::Id;

use crate::events::EventModifierFlag;
use crate::foundation::{id, nil, NSString, NSUInteger};

static BLOCK_PTR: &'static str = "cacaoMenuItemBlockPtr";

/// An Action is just an indirection layer to get around Rust and optimizing
/// zero-sum types; without this, pointers to callbacks will end up being
/// 0x1, and all point to whatever is there first (unsure if this is due to
/// Rust or Cocoa or what).
///
/// Point is, Button aren't created that much in the grand scheme of things,
/// and the heap isn't our enemy in a GUI framework anyway. If someone knows
/// a better way to do this that doesn't require double-boxing, I'm all ears.
pub struct Action(Box<dyn Fn() + 'static>);

impl fmt::Debug for Action {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let ptr = format!("{:p}", self.0);

        f.debug_struct("Action").field("fn", &ptr).finish()
    }
}

/// Internal method (shorthand) for generating `NSMenuItem` holders.
fn make_menu_item<S: AsRef<str>>(
    title: S,
    key: Option<&str>,
    action: Option<Sel>,
    modifiers: Option<&[EventModifierFlag]>
) -> Id<Object> {
    unsafe {
        let title = NSString::new(title.as_ref());

        // Note that AppKit requires a blank string if nil, not nil.
        let key = NSString::new(match key {
            Some(s) => s,
            None => ""
        });

        // Stock menu items that use selectors targeted at system pieces are just standard
        // `NSMenuItem`s. If there's no custom ones, we use our subclass that has a slot to store a
        // handler pointer.
        let alloc: id = msg_send![register_menu_item_class(), alloc];
        let item = Id::from_retained_ptr(match action {
            Some(a) => msg_send![alloc, initWithTitle:&*title action:a keyEquivalent:&*key],

            None => msg_send![alloc, initWithTitle:&*title
                action:sel!(fireBlockAction:)
                keyEquivalent:&*key]
        });

        if let Some(modifiers) = modifiers {
            let mut key_mask: NSUInteger = 0;

            for modifier in modifiers {
                let y: NSUInteger = modifier.into();
                key_mask = key_mask | y;
            }

            let _: () = msg_send![&*item, setKeyEquivalentModifierMask: key_mask];
        }

        item
    }
}

/// Represents varying `NSMenuItem` types - e.g, a separator vs an action. If you need something
/// outside of the stock item types, you can create a `Custom` variant that supports dispatching a
/// callback on the Rust side of things.
#[derive(Debug)]
pub enum MenuItem {
    /// A custom MenuItem. This type functions as a builder, so you can customize it easier.
    /// You can (and should) create this variant via the `new(title)` method, but if you need to do
    /// something crazier, then wrap it in this and you can hook into the Cacao menu system
    /// accordingly.
    Custom(Id<Object>),

    /// Shows a standard "About" item,  which will bring up the necessary window when clicked
    /// (include a `credits.html` in your App to make use of here). The argument baked in here
    /// should be your app name.
    About(String),

    /// A standard "hide the app" menu item.
    Hide,

    /// A standard "Services" menu item.
    Services,

    /// A "hide all other windows" menu item.
    HideOthers,

    /// A menu item to show all the windows for this app.
    ShowAll,

    /// Close the current window.
    CloseWindow,

    /// A "quit this app" menu icon.
    Quit,

    /// A menu item for enabling copying (often text) from responders.
    Copy,

    /// A menu item for enabling cutting (often text) from responders.
    Cut,

    /// An "undo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle
    /// of events.
    Undo,

    /// An "redo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle
    /// of events.
    Redo,

    /// A menu item for selecting all (often text) from responders.
    SelectAll,

    /// A menu item for pasting (often text) into responders.
    Paste,

    /// A standard "enter full screen" item.
    EnterFullScreen,

    /// An item for minimizing the window with the standard system controls.
    Minimize,

    /// An item for instructing the app to zoom. Your app must react to this with necessary window
    /// lifecycle events.
    Zoom,

    /// An item for automatically telling a SplitViewController to hide or show the sidebar. This
    /// only works on macOS 11.0+.
    ToggleSidebar,

    /// Represents a Separator. It's useful nonetheless for
    /// separating out pieces of the `NSMenu` structure.
    Separator
}

impl MenuItem {
    /// Consumes and returns a handle for the underlying MenuItem. This is internal as we make a few assumptions
    /// for how it interacts with our `Menu` setup, but this could be made public in the future.
    pub(crate) unsafe fn to_objc(self) -> Id<Object> {
        match self {
            Self::Custom(objc) => objc,

            Self::About(app_name) => {
                let title = format!("About {}", app_name);
                make_menu_item(&title, None, Some(sel!(orderFrontStandardAboutPanel:)), None)
            },

            Self::Hide => make_menu_item("Hide", Some("h"), Some(sel!(hide:)), None),

            // This one is a bit tricky to do right, as we need to expose a submenu, which isn't
            // supported by MenuItem yet.
            Self::Services => {
                let item = make_menu_item("Services", None, None, None);
                let app: id = msg_send![class!(RSTApplication), sharedApplication];
                let services: id = msg_send![app, servicesMenu];
                let _: () = msg_send![&*item, setSubmenu: services];
                item
            },

            Self::HideOthers => make_menu_item(
                "Hide Others",
                Some("h"),
                Some(sel!(hide:)),
                Some(&[EventModifierFlag::Command, EventModifierFlag::Option])
            ),

            Self::ShowAll => make_menu_item("Show All", None, Some(sel!(unhideAllApplications:)), None),
            Self::CloseWindow => make_menu_item("Close Window", Some("w"), Some(sel!(performClose:)), None),
            Self::Quit => make_menu_item("Quit", Some("q"), Some(sel!(terminate:)), None),
            Self::Copy => make_menu_item("Copy", Some("c"), Some(sel!(copy:)), None),
            Self::Cut => make_menu_item("Cut", Some("x"), Some(sel!(cut:)), None),
            Self::Undo => make_menu_item("Undo", Some("z"), Some(sel!(undo:)), None),
            Self::Redo => make_menu_item("Redo", Some("Z"), Some(sel!(redo:)), None),
            Self::SelectAll => make_menu_item("Select All", Some("a"), Some(sel!(selectAll:)), None),
            Self::Paste => make_menu_item("Paste", Some("v"), Some(sel!(paste:)), None),

            Self::EnterFullScreen => make_menu_item(
                "Enter Full Screen",
                Some("f"),
                Some(sel!(toggleFullScreen:)),
                Some(&[EventModifierFlag::Command, EventModifierFlag::Control])
            ),

            Self::Minimize => make_menu_item("Minimize", Some("m"), Some(sel!(performMiniaturize:)), None),
            Self::Zoom => make_menu_item("Zoom", None, Some(sel!(performZoom:)), None),

            Self::ToggleSidebar => make_menu_item(
                "Toggle Sidebar",
                Some("s"),
                Some(sel!(toggleSidebar:)),
                Some(&[EventModifierFlag::Command, EventModifierFlag::Option])
            ),

            Self::Separator => {
                let cls = class!(NSMenuItem);
                let separator: id = msg_send![cls, separatorItem];
                Id::from_ptr(separator)
            }
        }
    }

    /// Returns a `Custom` menu item, with the given title. You can configure this further with the
    /// builder methods on this object.
    pub fn new<S: AsRef<str>>(title: S) -> Self {
        MenuItem::Custom(make_menu_item(title, None, None, None))
    }

    /// Configures the a custom item to have specified key equivalent. This does nothing if called
    /// on a `MenuItem` type that is not `Custom`,
    pub fn key(self, key: &str) -> Self {
        if let MenuItem::Custom(objc) = self {
            unsafe {
                let key = NSString::new(key);
                let _: () = msg_send![&*objc, setKeyEquivalent: key];
            }

            return MenuItem::Custom(objc);
        }

        self
    }

    /// Sets the modifier key flags for this menu item. This does nothing if called on a `MenuItem`
    /// that is not `Custom`.
    pub fn modifiers(self, modifiers: &[EventModifierFlag]) -> Self {
        if let MenuItem::Custom(objc) = self {
            let mut key_mask: NSUInteger = 0;

            for modifier in modifiers {
                let y: NSUInteger = modifier.into();
                key_mask = key_mask | y;
            }

            unsafe {
                let _: () = msg_send![&*objc, setKeyEquivalentModifierMask: key_mask];
            }

            return MenuItem::Custom(objc);
        }

        self
    }

    /// Attaches a target/action handler to dispatch events. This does nothing if called on a
    /// `MenuItem` that is not `Custom`.
    ///
    /// Note that we use an extra bit of unsafety here to pass over a heap'd block. We need to do
    /// this as some menu items live in odd places (the system menu bar), and we need the handlers
    /// to persist. We inject a custom dealloc method to pull the pointer back and drop the handler
    /// whenever the menu item goes kaput.
    pub fn action<F: Fn() + 'static>(self, action: F) -> Self {
        if let MenuItem::Custom(mut objc) = self {
            let handler = Box::new(Action(Box::new(action)));
            let ptr = Box::into_raw(handler);

            unsafe {
                (&mut *objc).set_ivar(BLOCK_PTR, ptr as usize);
                let _: () = msg_send![&*objc, setTarget:&*objc];
            }

            return MenuItem::Custom(objc);
        }

        self
    }
}

/// On the Objective-C side, we need to ensure our handler is dropped when this subclass
/// is deallocated. Note that NSMenuItem is seemingly odd outside of ARC contexts, and we
/// need to do some extra logic to ensure release calls are properly sent.
extern "C" fn dealloc_cacao_menuitem(this: &Object, _: Sel) {
    unsafe {
        let ptr: usize = *this.get_ivar(BLOCK_PTR);
        let obj = ptr as *mut Action;

        if !obj.is_null() {
            let _handler = Box::from_raw(obj);
        }

        // This should be fine to _not_ do, but considering we go out of our way to loop it back on
        // itself, it's worth clearing out the slot.
        //let _: () = msg_send![this, setTarget:nil];

        let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc];
    }
}

/// Called when our custom item needs to fire.
extern "C" fn fire_block_action(this: &Object, _: Sel, _item: id) {
    let action = crate::utils::load::<Action>(this, BLOCK_PTR);
    (action.0)();
}

/// Injects a custom NSMenuItem subclass that contains a slot to hold a block, as well as a method
/// for calling the block.
///
/// In general, we do not want to do more than we need to here - menus are one of the last areas
/// where Carbon still lurks, and subclassing things can get weird.
pub(crate) fn register_menu_item_class() -> *const Class {
    static mut APP_CLASS: *const Class = 0 as *const Class;
    static INIT: Once = Once::new();

    INIT.call_once(|| unsafe {
        let superclass = class!(NSMenuItem);
        let mut decl = ClassDecl::new("CacaoMenuItem", superclass).unwrap();
        decl.add_ivar::<usize>(BLOCK_PTR);

        decl.add_method(sel!(dealloc), dealloc_cacao_menuitem as extern "C" fn(&Object, _));
        decl.add_method(sel!(fireBlockAction:), fire_block_action as extern "C" fn(&Object, _, id));

        APP_CLASS = decl.register();
    });

    unsafe { APP_CLASS }
}