Skip to main content

nifty_macos/
classes.rs

1//! Objective-C classes Nifty registers to receive `AppKit` callbacks.
2
3use std::cell::Cell;
4use std::fmt;
5
6use objc2::rc::{Retained, Weak};
7use objc2::runtime::{AnyObject, ProtocolObject, Sel};
8use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
9use objc2_app_kit::{
10    NSApplication, NSApplicationDelegate, NSControl, NSControlStateValue, NSControlStateValueOff,
11    NSControlStateValueOn, NSControlTextEditingDelegate, NSDragOperation, NSDraggingDestination,
12    NSDraggingInfo, NSEvent, NSMenu, NSMenuItem, NSPopUpButton,
13    NSPreferredScrollerStyleDidChangeNotification, NSSlider, NSTextDelegate, NSTextField,
14    NSTextFieldDelegate, NSTextView, NSTextViewDelegate, NSView, NSWindow, NSWindowDelegate,
15};
16use objc2_foundation::{NSNotification, NSNotificationCenter, NSObject, NSObjectProtocol};
17
18use nifty_runtime::{
19    ClickHandler, CloseHandler, MenuHandler, NumberHandler, ResizeHandler, TextHandler,
20    ToggleHandler,
21};
22
23use crate::context_menu;
24use crate::convert::to_size;
25use crate::drop;
26
27define_class!(
28    // SAFETY: NSView has no subclassing requirements and `FlippedView` does not implement Drop.
29    #[unsafe(super = NSView)]
30    #[thread_kind = MainThreadOnly]
31    #[name = "NiftyFlippedView"]
32    #[derive(Debug)]
33    pub(crate) struct FlippedView;
34
35    impl FlippedView {
36        /// Puts the origin at the top-left, matching Nifty's layout coordinates.
37        #[unsafe(method(isFlipped))]
38        fn is_flipped(&self) -> bool {
39            true
40        }
41
42        /// Widgets sit straight in this view, so a right-click on one that has no menu
43        /// of its own comes here, to the menu of the stack under it.
44        #[unsafe(method_id(menuForEvent:))]
45        fn menu_for_event(&self, event: &NSEvent) -> Option<Retained<NSMenu>> {
46            context_menu::menu_at(self, event)
47        }
48    }
49
50    // SAFETY: NSObjectProtocol has no safety requirements.
51    unsafe impl NSObjectProtocol for FlippedView {}
52
53    // SAFETY: the methods take and return what NSDraggingDestination declares.
54    unsafe impl NSDraggingDestination for FlippedView {
55        #[unsafe(method(draggingEntered:))]
56        fn dragging_entered(&self, drag: &ProtocolObject<dyn NSDraggingInfo>) -> NSDragOperation {
57            drop::drag_operation(self, drag)
58        }
59
60        #[unsafe(method(draggingUpdated:))]
61        fn dragging_updated(&self, drag: &ProtocolObject<dyn NSDraggingInfo>) -> NSDragOperation {
62            drop::drag_operation(self, drag)
63        }
64
65        #[unsafe(method(performDragOperation:))]
66        fn perform_drag_operation(&self, drag: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
67            drop::perform_drop(self, drag)
68        }
69    }
70);
71
72impl FlippedView {
73    pub(crate) fn new(mtm: MainThreadMarker) -> Retained<Self> {
74        let this = Self::alloc(mtm).set_ivars(());
75        // SAFETY: `init` is NSView's designated initializer and takes no arguments.
76        unsafe { msg_send![super(this), init] }
77    }
78}
79
80pub(crate) struct ClickTargetIvars {
81    on_click: ClickHandler,
82    /// Whether the button it hears from shows as selected, for one that shows that.
83    selected: Option<Cell<bool>>,
84}
85
86impl fmt::Debug for ClickTargetIvars {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.debug_struct("ClickTargetIvars").finish_non_exhaustive()
89    }
90}
91
92define_class!(
93    // SAFETY: NSObject has no subclassing requirements and `ClickTarget` does not implement Drop.
94    #[unsafe(super = NSObject)]
95    #[thread_kind = MainThreadOnly]
96    #[name = "NiftyClickTarget"]
97    #[ivars = ClickTargetIvars]
98    #[derive(Debug)]
99    pub(crate) struct ClickTarget;
100
101    impl ClickTarget {
102        /// An on-off button flips its own state when clicked, so it is put back to the
103        /// selection the click may have changed, through `set_selected`.
104        #[unsafe(method(click:))]
105        fn click(&self, sender: Option<&AnyObject>) {
106            let ivars = self.ivars();
107            (ivars.on_click)();
108            if let (Some(selected), Some(sender)) = (&ivars.selected, sender) {
109                // SAFETY: only buttons send `click:`, and on-off buttons take a state.
110                let () = unsafe { msg_send![sender, setState: control_state(selected.get())] };
111            }
112        }
113    }
114);
115
116impl ClickTarget {
117    pub(crate) fn action() -> Sel {
118        sel!(click:)
119    }
120
121    pub(crate) fn new(mtm: MainThreadMarker, on_click: ClickHandler) -> Retained<Self> {
122        Self::with_ivars(
123            mtm,
124            ClickTargetIvars {
125                on_click,
126                selected: None,
127            },
128        )
129    }
130
131    /// The target of a button that shows whether it is `selected`.
132    pub(crate) fn selecting(
133        mtm: MainThreadMarker,
134        on_click: ClickHandler,
135        selected: bool,
136    ) -> Retained<Self> {
137        Self::with_ivars(
138            mtm,
139            ClickTargetIvars {
140                on_click,
141                selected: Some(Cell::new(selected)),
142            },
143        )
144    }
145
146    pub(crate) fn set_selected(&self, selected: bool) {
147        if let Some(shown) = &self.ivars().selected {
148            shown.set(selected);
149        }
150    }
151
152    fn with_ivars(mtm: MainThreadMarker, ivars: ClickTargetIvars) -> Retained<Self> {
153        let this = Self::alloc(mtm).set_ivars(ivars);
154        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
155        unsafe { msg_send![super(this), init] }
156    }
157}
158
159/// The state of an on-off control, ticked or selected when `on`.
160pub(crate) fn control_state(on: bool) -> NSControlStateValue {
161    if on {
162        NSControlStateValueOn
163    } else {
164        NSControlStateValueOff
165    }
166}
167
168pub(crate) struct ToggleTargetIvars {
169    on_toggle: ToggleHandler,
170}
171
172impl fmt::Debug for ToggleTargetIvars {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        f.debug_struct("ToggleTargetIvars").finish_non_exhaustive()
175    }
176}
177
178define_class!(
179    // SAFETY: NSObject has no subclassing requirements and `ToggleTarget` does not implement Drop.
180    #[unsafe(super = NSObject)]
181    #[thread_kind = MainThreadOnly]
182    #[name = "NiftyToggleTarget"]
183    #[ivars = ToggleTargetIvars]
184    #[derive(Debug)]
185    pub(crate) struct ToggleTarget;
186
187    impl ToggleTarget {
188        #[unsafe(method(toggle:))]
189        fn toggle(&self, sender: &AnyObject) {
190            // SAFETY: only checkboxes and switches send `toggle:`, and both have a state.
191            let state: NSControlStateValue = unsafe { msg_send![sender, state] };
192            (self.ivars().on_toggle)(state == NSControlStateValueOn);
193        }
194    }
195);
196
197impl ToggleTarget {
198    pub(crate) fn action() -> Sel {
199        sel!(toggle:)
200    }
201
202    pub(crate) fn new(mtm: MainThreadMarker, on_toggle: ToggleHandler) -> Retained<Self> {
203        let this = Self::alloc(mtm).set_ivars(ToggleTargetIvars { on_toggle });
204        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
205        unsafe { msg_send![super(this), init] }
206    }
207}
208
209pub(crate) struct MenuTargetIvars {
210    on_select: MenuHandler,
211}
212
213impl fmt::Debug for MenuTargetIvars {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        f.debug_struct("MenuTargetIvars").finish_non_exhaustive()
216    }
217}
218
219define_class!(
220    // SAFETY: NSObject has no subclassing requirements and `MenuTarget` does not implement Drop.
221    #[unsafe(super = NSObject)]
222    #[thread_kind = MainThreadOnly]
223    #[name = "NiftyMenuTarget"]
224    #[ivars = MenuTargetIvars]
225    #[derive(Debug)]
226    pub(crate) struct MenuTarget;
227
228    impl MenuTarget {
229        /// Each item of the app's menus is numbered by its tag.
230        #[unsafe(method(choose:))]
231        fn choose(&self, sender: &NSMenuItem) {
232            let item = usize::try_from(sender.tag()).expect("menu items are numbered from 0");
233            (self.ivars().on_select)(item);
234        }
235    }
236);
237
238impl MenuTarget {
239    pub(crate) fn action() -> Sel {
240        sel!(choose:)
241    }
242
243    pub(crate) fn new(mtm: MainThreadMarker, on_select: MenuHandler) -> Retained<Self> {
244        let this = Self::alloc(mtm).set_ivars(MenuTargetIvars { on_select });
245        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
246        unsafe { msg_send![super(this), init] }
247    }
248}
249
250pub(crate) struct NumberTargetIvars {
251    on_change: NumberHandler,
252    /// The whole number last shown, so dragging within one reports nothing new.
253    last: Cell<i64>,
254}
255
256impl fmt::Debug for NumberTargetIvars {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        f.debug_struct("NumberTargetIvars")
259            .field("last", &self.last)
260            .finish_non_exhaustive()
261    }
262}
263
264define_class!(
265    // SAFETY: NSObject has no subclassing requirements and `NumberTarget` does not implement Drop.
266    #[unsafe(super = NSObject)]
267    #[thread_kind = MainThreadOnly]
268    #[name = "NiftyNumberTarget"]
269    #[ivars = NumberTargetIvars]
270    #[derive(Debug)]
271    pub(crate) struct NumberTarget;
272
273    impl NumberTarget {
274        /// Sliders slide smoothly, and report the whole number nearest the knob.
275        #[unsafe(method(change:))]
276        fn change(&self, sender: &NSSlider) {
277            let value = whole(sender.doubleValue());
278            let ivars = self.ivars();
279            if ivars.last.replace(value) != value {
280                (ivars.on_change)(value);
281            }
282        }
283    }
284);
285
286impl NumberTarget {
287    pub(crate) fn action() -> Sel {
288        sel!(change:)
289    }
290
291    pub(crate) fn new(
292        mtm: MainThreadMarker,
293        value: i64,
294        on_change: NumberHandler,
295    ) -> Retained<Self> {
296        let ivars = NumberTargetIvars {
297            on_change,
298            last: Cell::new(value),
299        };
300        let this = Self::alloc(mtm).set_ivars(ivars);
301        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
302        unsafe { msg_send![super(this), init] }
303    }
304
305    /// Records a value shown from code, which the user didn't pick.
306    pub(crate) fn shown(&self, value: i64) {
307        self.ivars().last.set(value);
308    }
309}
310
311#[expect(
312    clippy::cast_possible_truncation,
313    reason = "a slider's value lies between bounds the checker keeps far inside i64"
314)]
315fn whole(value: f64) -> i64 {
316    value.round() as i64
317}
318
319pub(crate) struct PickTargetIvars {
320    on_pick: TextHandler,
321}
322
323impl fmt::Debug for PickTargetIvars {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        f.debug_struct("PickTargetIvars").finish_non_exhaustive()
326    }
327}
328
329define_class!(
330    // SAFETY: NSObject has no subclassing requirements and `PickTarget` does not implement Drop.
331    #[unsafe(super = NSObject)]
332    #[thread_kind = MainThreadOnly]
333    #[name = "NiftyPickTarget"]
334    #[ivars = PickTargetIvars]
335    #[derive(Debug)]
336    pub(crate) struct PickTarget;
337
338    impl PickTarget {
339        #[unsafe(method(pick:))]
340        fn pick(&self, sender: &NSPopUpButton) {
341            if let Some(title) = sender.titleOfSelectedItem() {
342                (self.ivars().on_pick)(&title.to_string());
343            }
344        }
345    }
346);
347
348impl PickTarget {
349    pub(crate) fn action() -> Sel {
350        sel!(pick:)
351    }
352
353    pub(crate) fn new(mtm: MainThreadMarker, on_pick: TextHandler) -> Retained<Self> {
354        let this = Self::alloc(mtm).set_ivars(PickTargetIvars { on_pick });
355        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
356        unsafe { msg_send![super(this), init] }
357    }
358}
359
360pub(crate) struct TextFieldDelegateIvars {
361    field: Weak<NSTextField>,
362    on_change: TextHandler,
363    /// What Return runs in the field, in place of the window's default button.
364    on_submit: Option<ClickHandler>,
365}
366
367impl fmt::Debug for TextFieldDelegateIvars {
368    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369        f.debug_struct("TextFieldDelegateIvars")
370            .finish_non_exhaustive()
371    }
372}
373
374define_class!(
375    // SAFETY: NSObject has no subclassing requirements and `TextFieldDelegate` does not implement Drop.
376    #[unsafe(super = NSObject)]
377    #[thread_kind = MainThreadOnly]
378    #[name = "NiftyTextFieldDelegate"]
379    #[ivars = TextFieldDelegateIvars]
380    #[derive(Debug)]
381    pub(crate) struct TextFieldDelegate;
382
383    // SAFETY: NSObjectProtocol has no safety requirements.
384    unsafe impl NSObjectProtocol for TextFieldDelegate {}
385
386    // SAFETY: NSControlTextEditingDelegate has no safety requirements.
387    unsafe impl NSControlTextEditingDelegate for TextFieldDelegate {
388        #[unsafe(method(controlTextDidChange:))]
389        fn control_text_did_change(&self, _notification: &NSNotification) {
390            let ivars = self.ivars();
391            if let Some(field) = ivars.field.load() {
392                (ivars.on_change)(&field.stringValue().to_string());
393            }
394        }
395
396        /// Return in a field with an action runs it, and answers that Return is handled,
397        /// so the window's default button isn't pressed too.
398        #[unsafe(method(control:textView:doCommandBySelector:))]
399        fn do_command(&self, _control: &NSControl, _editor: &NSTextView, command: Sel) -> bool {
400            match &self.ivars().on_submit {
401                Some(on_submit) if command == sel!(insertNewline:) => {
402                    on_submit();
403                    true
404                }
405                _ => false,
406            }
407        }
408    }
409
410    // SAFETY: NSTextFieldDelegate has no safety requirements.
411    unsafe impl NSTextFieldDelegate for TextFieldDelegate {}
412);
413
414impl TextFieldDelegate {
415    /// Only edits made by the user notify the delegate; setting the value in code does not.
416    pub(crate) fn new(
417        mtm: MainThreadMarker,
418        field: &Retained<NSTextField>,
419        on_change: TextHandler,
420        on_submit: Option<ClickHandler>,
421    ) -> Retained<Self> {
422        let ivars = TextFieldDelegateIvars {
423            field: Weak::from_retained(field),
424            on_change,
425            on_submit,
426        };
427        let this = Self::alloc(mtm).set_ivars(ivars);
428        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
429        unsafe { msg_send![super(this), init] }
430    }
431}
432
433pub(crate) struct TextViewDelegateIvars {
434    text_view: Weak<NSTextView>,
435    on_change: TextHandler,
436}
437
438impl fmt::Debug for TextViewDelegateIvars {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        f.debug_struct("TextViewDelegateIvars")
441            .finish_non_exhaustive()
442    }
443}
444
445define_class!(
446    // SAFETY: NSObject has no subclassing requirements and `TextViewDelegate` does not implement Drop.
447    #[unsafe(super = NSObject)]
448    #[thread_kind = MainThreadOnly]
449    #[name = "NiftyTextViewDelegate"]
450    #[ivars = TextViewDelegateIvars]
451    #[derive(Debug)]
452    pub(crate) struct TextViewDelegate;
453
454    // SAFETY: NSObjectProtocol has no safety requirements.
455    unsafe impl NSObjectProtocol for TextViewDelegate {}
456
457    // SAFETY: NSTextDelegate has no safety requirements.
458    unsafe impl NSTextDelegate for TextViewDelegate {
459        #[unsafe(method(textDidChange:))]
460        fn text_did_change(&self, _notification: &NSNotification) {
461            let ivars = self.ivars();
462            if let Some(text_view) = ivars.text_view.load() {
463                (ivars.on_change)(&text_view.string().to_string());
464            }
465        }
466    }
467
468    // SAFETY: NSTextViewDelegate has no safety requirements.
469    unsafe impl NSTextViewDelegate for TextViewDelegate {}
470);
471
472impl TextViewDelegate {
473    /// Only edits made by the user notify the delegate; setting the text in code does not.
474    pub(crate) fn new(
475        mtm: MainThreadMarker,
476        text_view: &Retained<NSTextView>,
477        on_change: TextHandler,
478    ) -> Retained<Self> {
479        let ivars = TextViewDelegateIvars {
480            text_view: Weak::from_retained(text_view),
481            on_change,
482        };
483        let this = Self::alloc(mtm).set_ivars(ivars);
484        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
485        unsafe { msg_send![super(this), init] }
486    }
487}
488
489pub(crate) struct WindowDelegateIvars {
490    content: Retained<FlippedView>,
491    on_resize: ResizeHandler,
492    on_close: CloseHandler,
493}
494
495impl fmt::Debug for WindowDelegateIvars {
496    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497        f.debug_struct("WindowDelegateIvars")
498            .field("content", &self.content)
499            .finish_non_exhaustive()
500    }
501}
502
503define_class!(
504    // SAFETY: NSObject has no subclassing requirements and `WindowDelegate` does not implement Drop.
505    #[unsafe(super = NSObject)]
506    #[thread_kind = MainThreadOnly]
507    #[name = "NiftyWindowDelegate"]
508    #[ivars = WindowDelegateIvars]
509    #[derive(Debug)]
510    pub(crate) struct WindowDelegate;
511
512    // SAFETY: NSObjectProtocol has no safety requirements.
513    unsafe impl NSObjectProtocol for WindowDelegate {}
514
515    // SAFETY: NSWindowDelegate has no safety requirements.
516    unsafe impl NSWindowDelegate for WindowDelegate {
517        #[unsafe(method(windowDidResize:))]
518        fn window_did_resize(&self, _notification: &NSNotification) {
519            self.report_size();
520        }
521
522        /// The user asked to close the window; the app closes it, once it has taken its
523        /// content down.
524        #[unsafe(method(windowShouldClose:))]
525        fn window_should_close(&self, _sender: &NSWindow) -> bool {
526            (self.ivars().on_close)();
527            false
528        }
529    }
530
531    impl WindowDelegate {
532        /// Scroll bars that float over the content became ones that take a strip beside
533        /// it, or the other way round, so the content is laid out again.
534        #[unsafe(method(preferredScrollerStyleDidChange:))]
535        fn preferred_scroller_style_did_change(&self, _notification: &NSNotification) {
536            self.report_size();
537        }
538    }
539);
540
541impl WindowDelegate {
542    pub(crate) fn new(
543        mtm: MainThreadMarker,
544        content: Retained<FlippedView>,
545        on_resize: ResizeHandler,
546        on_close: CloseHandler,
547    ) -> Retained<Self> {
548        let this = Self::alloc(mtm).set_ivars(WindowDelegateIvars {
549            content,
550            on_resize,
551            on_close,
552        });
553        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
554        let delegate: Retained<Self> = unsafe { msg_send![super(this), init] };
555        // SAFETY: the delegate implements the selector, which takes the notification, and
556        // the center stops posting to an observer once it is deallocated.
557        unsafe {
558            NSNotificationCenter::defaultCenter().addObserver_selector_name_object(
559                &delegate,
560                sel!(preferredScrollerStyleDidChange:),
561                Some(NSPreferredScrollerStyleDidChangeNotification),
562                None,
563            );
564        }
565        delegate
566    }
567
568    fn report_size(&self) {
569        let ivars = self.ivars();
570        (ivars.on_resize)(to_size(ivars.content.frame().size));
571    }
572}
573
574define_class!(
575    // SAFETY: NSObject has no subclassing requirements and `AppDelegate` does not implement Drop.
576    #[unsafe(super = NSObject)]
577    #[thread_kind = MainThreadOnly]
578    #[name = "NiftyAppDelegate"]
579    #[derive(Debug)]
580    pub(crate) struct AppDelegate;
581
582    // SAFETY: NSObjectProtocol has no safety requirements.
583    unsafe impl NSObjectProtocol for AppDelegate {}
584
585    // SAFETY: NSApplicationDelegate has no safety requirements.
586    unsafe impl NSApplicationDelegate for AppDelegate {
587        /// Apps launched from a terminal start in the background unless activated.
588        #[unsafe(method(applicationDidFinishLaunching:))]
589        fn did_finish_launching(&self, _notification: &NSNotification) {
590            NSApplication::sharedApplication(self.mtm()).activate();
591        }
592
593        #[unsafe(method(applicationShouldTerminateAfterLastWindowClosed:))]
594        fn should_terminate_after_last_window_closed(&self, _sender: &NSApplication) -> bool {
595            true
596        }
597    }
598);
599
600impl AppDelegate {
601    pub(crate) fn new(mtm: MainThreadMarker) -> Retained<Self> {
602        let this = Self::alloc(mtm).set_ivars(());
603        // SAFETY: `init` is NSObject's designated initializer and takes no arguments.
604        unsafe { msg_send![super(this), init] }
605    }
606}