1use 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 #[unsafe(super = NSView)]
30 #[thread_kind = MainThreadOnly]
31 #[name = "NiftyFlippedView"]
32 #[derive(Debug)]
33 pub(crate) struct FlippedView;
34
35 impl FlippedView {
36 #[unsafe(method(isFlipped))]
38 fn is_flipped(&self) -> bool {
39 true
40 }
41
42 #[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 unsafe impl NSObjectProtocol for FlippedView {}
52
53 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 unsafe { msg_send![super(this), init] }
77 }
78}
79
80pub(crate) struct ClickTargetIvars {
81 on_click: ClickHandler,
82 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 #[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 #[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 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 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 unsafe { msg_send![super(this), init] }
156 }
157}
158
159pub(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 #[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 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 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 #[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 #[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 unsafe { msg_send![super(this), init] }
247 }
248}
249
250pub(crate) struct NumberTargetIvars {
251 on_change: NumberHandler,
252 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 #[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 #[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 unsafe { msg_send![super(this), init] }
303 }
304
305 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 #[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 unsafe { msg_send![super(this), init] }
357 }
358}
359
360pub(crate) struct TextFieldDelegateIvars {
361 field: Weak<NSTextField>,
362 on_change: TextHandler,
363 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 #[unsafe(super = NSObject)]
377 #[thread_kind = MainThreadOnly]
378 #[name = "NiftyTextFieldDelegate"]
379 #[ivars = TextFieldDelegateIvars]
380 #[derive(Debug)]
381 pub(crate) struct TextFieldDelegate;
382
383 unsafe impl NSObjectProtocol for TextFieldDelegate {}
385
386 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 #[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 unsafe impl NSTextFieldDelegate for TextFieldDelegate {}
412);
413
414impl TextFieldDelegate {
415 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 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 #[unsafe(super = NSObject)]
448 #[thread_kind = MainThreadOnly]
449 #[name = "NiftyTextViewDelegate"]
450 #[ivars = TextViewDelegateIvars]
451 #[derive(Debug)]
452 pub(crate) struct TextViewDelegate;
453
454 unsafe impl NSObjectProtocol for TextViewDelegate {}
456
457 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 unsafe impl NSTextViewDelegate for TextViewDelegate {}
470);
471
472impl TextViewDelegate {
473 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 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 #[unsafe(super = NSObject)]
506 #[thread_kind = MainThreadOnly]
507 #[name = "NiftyWindowDelegate"]
508 #[ivars = WindowDelegateIvars]
509 #[derive(Debug)]
510 pub(crate) struct WindowDelegate;
511
512 unsafe impl NSObjectProtocol for WindowDelegate {}
514
515 unsafe impl NSWindowDelegate for WindowDelegate {
517 #[unsafe(method(windowDidResize:))]
518 fn window_did_resize(&self, _notification: &NSNotification) {
519 self.report_size();
520 }
521
522 #[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 #[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 let delegate: Retained<Self> = unsafe { msg_send![super(this), init] };
555 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 #[unsafe(super = NSObject)]
577 #[thread_kind = MainThreadOnly]
578 #[name = "NiftyAppDelegate"]
579 #[derive(Debug)]
580 pub(crate) struct AppDelegate;
581
582 unsafe impl NSObjectProtocol for AppDelegate {}
584
585 unsafe impl NSApplicationDelegate for AppDelegate {
587 #[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 unsafe { msg_send![super(this), init] }
605 }
606}