1 /**
2 * Cocoa window implementation.
3 * Copyright: Copyright Guillaume Piolat 2015 - 2021.
4 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
5 */
6 module dplug.window.cocoawindow;
7 
8 import core.stdc.stdlib;
9 
10 import std.string;
11 import std.uuid;
12 
13 import dplug.math.vector;
14 import dplug.math.box;
15 import dplug.graphics.image;
16 
17 import dplug.core.sync;
18 import dplug.core.runtime;
19 import dplug.core.nogc;
20 import dplug.core.random;
21 import dplug.core.thread;
22 import dplug.window.window;
23 
24 import derelict.cocoa;
25 
26 
27 version = useCoreGraphicsContext;
28 
29 final class CocoaWindow : IWindow
30 {
31 nothrow:
32 @nogc:
33 private:
34     IWindowListener _listener;
35 
36     NSColorSpace _nsColorSpace;
37     CGColorSpaceRef _cgColorSpaceRef;
38     NSData _imageData;
39     NSString _logFormatStr;
40 
41     // Only used by host windows
42     NSWindow _nsWindow;
43     NSApplication _nsApplication;
44 
45     DPlugCustomView _view = null;
46 
47     bool _terminated = false;
48 
49     int _lastMouseX, _lastMouseY;
50     bool _firstMouseMove = true;
51 
52     int _width;
53     int _height;
54 
55     ImageRef!RGBA _wfb;
56 
57     uint _timeAtCreationInMs;
58     uint _lastMeasturedTimeInMs;
59     bool _dirtyAreasAreNotYetComputed;
60 
61     bool _isHostWindow;
62     bool _drawRectWorkaround; // See Issue #505 & #705, drawRect: returning always one big rectangle, killing CPU
63 
64     MouseCursor _lastMouseCursor;
65 
66 public:
67 
68     this(WindowUsage usage, void* parentWindow, IWindowListener listener, int width, int height)
69     {
70         _isHostWindow = (usage == WindowUsage.host);
71 
72         _listener = listener;
73 
74         acquireCocoaFunctions();
75         acquireCoreGraphicsFunctions();
76         NSApplicationLoad(); // to use Cocoa in Carbon applications
77         bool parentViewExists = parentWindow !is null;
78 
79         _width = 0;
80         _height = 0;
81 
82         _nsColorSpace = NSColorSpace.sRGBColorSpace();
83         // hopefully not null else the colors will be brighter
84         _cgColorSpaceRef = _nsColorSpace.CGColorSpace();
85 
86         _logFormatStr = NSString.stringWith("%@");
87 
88         _timeAtCreationInMs = getTimeMs();
89         _lastMeasturedTimeInMs = _timeAtCreationInMs;
90 
91         _dirtyAreasAreNotYetComputed = true;
92 
93         // The drawRect: failure of having small rectangles started with 11.0 Big Sur beta 9.
94         // It was afterwards removed for Issue #705, it's no longer useful in Monterey, and in Ardour was making things worse.
95         version(OSX)
96             _drawRectWorkaround = (getMacOSVersion().major == 11);
97 
98         if (!_isHostWindow)
99         {
100             DPlugCustomView.generateClassName();
101             DPlugCustomView.registerSubclass();
102 
103             _view = DPlugCustomView.alloc();
104             _view.initialize(this, width, height);
105 
106             // GOAL: Force display by the GPU, this is supposed to solve
107             // resampling problems on HiDPI like 4k and 5k
108             // REALITY: QA reports this to be blurrier AND slower than previously
109             // Layer has to be there for the drawRect workaround.
110             if (_drawRectWorkaround)
111                 _view.setWantsLayer(YES);
112 
113             //_view.layer.setDrawsAsynchronously(YES);
114             // This is supposed to make things faster, but doesn't
115             //_view.layer.setOpaque(YES);
116 
117             // In VST, add the view to the parent view.
118             // In AU (parentWindow == null), a reference to the view is returned instead and the host does it.
119             if (parentWindow !is null)
120             {
121                 NSView parentView = NSView(cast(id)parentWindow);
122                 parentView.addSubview(_view);
123             }
124 
125             // See Issue #688: when changing the buffer size or sampling rate,
126             // Logic destroy and reloads the plugin, with same settings. The window
127             // is reused, thus layout doesn't get called and the plugin is unsized!
128             layout();
129         }
130         else
131         {
132             _nsApplication = NSApplication.sharedApplication;
133             _nsApplication.setActivationPolicy(NSApplicationActivationPolicyRegular);
134             _nsWindow = NSWindow.alloc();
135             _nsWindow.initWithContentRect(NSMakeRect(0, 0, width, height),
136                                             NSBorderlessWindowMask, NSBackingStoreBuffered, NO);
137             _nsWindow.makeKeyAndOrderFront();
138             _nsApplication.activateIgnoringOtherApps(YES);
139         }
140     }
141 
142     ~this()
143     {
144         if (!_isHostWindow)
145         {
146             _terminated = true;
147 
148             {
149                 _view.killTimer();
150             }
151 
152             _view.removeFromSuperview();
153             _view.release();
154             _view = DPlugCustomView(null);
155 
156             DPlugCustomView.unregisterSubclass();
157         }
158         else
159         {
160             _nsWindow.destroy();
161         }
162 
163         releaseCocoaFunctions();
164     }
165 
166     // Implements IWindow
167     override void waitEventAndDispatch()
168     {
169         if (!_isHostWindow)
170             assert(false); // only valid for a host window
171 
172         NSEvent event = _nsWindow.nextEventMatchingMask(cast(NSUInteger)-1);
173         _nsApplication.sendEvent(event);
174     }
175 
176     override bool terminated()
177     {
178         return _terminated;
179     }
180 
181     override uint getTimeMs()
182     {
183         double timeMs = NSDate.timeIntervalSinceReferenceDate() * 1000.0;
184 
185         // WARNING: ARM and x86 do not convert float to int in the same way.
186         // Convert to 64-bit and use integer truncation rather than UB.
187         // See: https://github.com/ldc-developers/ldc/issues/3603
188         long timeMs_integer = cast(long)timeMs;
189         uint ms = cast(uint)(timeMs_integer);
190         return ms;
191     }
192 
193     override void* systemHandle()
194     {
195         if (_isHostWindow)
196             return _nsWindow.contentView()._id; // return the main NSView
197         else
198             return _view._id;
199     }
200 
201     override bool requestResize(int widthLogicalPixels, int heightLogicalPixels, bool alsoResizeParentWindow)
202     {
203         assert(!alsoResizeParentWindow); // unsupported here
204         NSSize size = NSSize(cast(CGFloat)widthLogicalPixels,
205                              cast(CGFloat)heightLogicalPixels);
206         _view.setFrameSize(size);
207         return true;
208     }
209 
210 private:
211 
212     MouseState getMouseState(NSEvent event)
213     {
214         // not working
215         MouseState state;
216         uint pressedMouseButtons = event.pressedMouseButtons();
217         if (pressedMouseButtons & 1)
218             state.leftButtonDown = true;
219         if (pressedMouseButtons & 2)
220             state.rightButtonDown = true;
221         if (pressedMouseButtons & 4)
222             state.middleButtonDown = true;
223 
224         NSEventModifierFlags mod = event.modifierFlags();
225         if (mod & NSControlKeyMask)
226             state.ctrlPressed = true;
227         if (mod & NSShiftKeyMask)
228             state.shiftPressed = true;
229         if (mod & NSAlternateKeyMask)
230             state.altPressed = true;
231 
232         return state;
233     }
234 
235     void handleMouseWheel(NSEvent event)
236     {
237         double ddeltaX = event.deltaX;
238         double ddeltaY = event.deltaY;
239         int deltaX = 0;
240         int deltaY = 0;
241         if (ddeltaX > 0) deltaX = 1;
242         if (ddeltaX < 0) deltaX = -1;
243         if (ddeltaY > 0) deltaY = 1;
244         if (ddeltaY < 0) deltaY = -1;
245         if (deltaX || deltaY)
246         {
247             vec2i mousePos = getMouseXY(_view, event, _height);
248             _listener.onMouseWheel(mousePos.x, mousePos.y, deltaX, deltaY, getMouseState(event));
249         }
250     }
251 
252     bool handleKeyEvent(NSEvent event, bool released)
253     {
254         uint keyCode = event.keyCode();
255         Key key;
256         switch (keyCode)
257         {
258             case kVK_ANSI_Keypad0: key = Key.digit0; break;
259             case kVK_ANSI_Keypad1: key = Key.digit1; break;
260             case kVK_ANSI_Keypad2: key = Key.digit2; break;
261             case kVK_ANSI_Keypad3: key = Key.digit3; break;
262             case kVK_ANSI_Keypad4: key = Key.digit4; break;
263             case kVK_ANSI_Keypad5: key = Key.digit5; break;
264             case kVK_ANSI_Keypad6: key = Key.digit6; break;
265             case kVK_ANSI_Keypad7: key = Key.digit7; break;
266             case kVK_ANSI_Keypad8: key = Key.digit8; break;
267             case kVK_ANSI_Keypad9: key = Key.digit9; break;
268             case kVK_Return: key = Key.enter; break;
269             case kVK_Escape: key = Key.escape; break;
270             case kVK_LeftArrow: key = Key.leftArrow; break;
271             case kVK_RightArrow: key = Key.rightArrow; break;
272             case kVK_DownArrow: key = Key.downArrow; break;
273             case kVK_UpArrow: key = Key.upArrow; break;
274             case kVK_Delete: key = Key.backspace; break;
275             case kVK_ForwardDelete: key = Key.suppr; break;
276             default:
277             {
278                 NSString characters = event.charactersIgnoringModifiers();
279                 if (characters.length() == 0)
280                 {
281                     key = Key.unsupported;
282                 }
283                 else
284                 {
285                     wchar ch = characters.characterAtIndex(0);
286                     if (ch >= '0' && ch <= '9')
287                         key = cast(Key)(Key.digit0 + (ch - '0'));
288                     else if (ch >= 'A' && ch <= 'Z')
289                         key = cast(Key)(Key.A + (ch - 'A'));
290                     else if (ch >= 'a' && ch <= 'z')
291                         key = cast(Key)(Key.a + (ch - 'a'));
292                     else
293                         key = Key.unsupported;
294                 }
295             }
296         }
297 
298         bool handled = false;
299 
300         if (released)
301         {
302             if (_listener.onKeyDown(key))
303                 handled = true;
304         }
305         else
306         {
307             if (_listener.onKeyUp(key))
308                 handled = true;
309         }
310 
311         return handled;
312     }
313 
314     void handleMouseMove(NSEvent event)
315     {
316         vec2i mousePos = getMouseXY(_view, event, _height);
317 
318         if (_firstMouseMove)
319         {
320             _firstMouseMove = false;
321             _lastMouseX = mousePos.x;
322             _lastMouseY = mousePos.y;
323         }
324 
325         _listener.onMouseMove(mousePos.x, mousePos.y, mousePos.x - _lastMouseX, mousePos.y - _lastMouseY,
326             getMouseState(event));
327 
328         version(legacyMouseCursor)
329         {}
330         else
331         {
332             setMouseCursor(_listener.getMouseCursor());
333         }
334 
335         _lastMouseX = mousePos.x;
336         _lastMouseY = mousePos.y;
337     }
338 
339     void handleMouseEntered(NSEvent event)
340     {
341         // Welcome to Issue #737.
342         //
343         // Consider the mouse cursor has changed, since the mouse was elsewhere.
344         // Give it an impossible mouse cursor cached value.
345         _lastMouseCursor = cast(MouseCursor) -1;
346 
347         // Furthermore, because:
348         // 1. either the cursor might not be set by subsequent
349         // 2. either the mouseMove event might not be called, because the window is hosted in another process
350         //    and isn't active yet (macOS has "click through", a sort of mouse focus)
351         // then we need to set the mouse cursor upon entry. Tricky!
352         version(legacyMouseCursor)
353         {
354             setMouseCursor(MouseCursor.pointer);
355         }
356         else
357         {
358             setMouseCursor(_listener.getMouseCursor());
359         }
360     }
361 
362     void handleMouseClicks(NSEvent event, MouseButton mb, bool released)
363     {
364         vec2i mousePos = getMouseXY(_view, event, _height);
365 
366         if (released)
367             _listener.onMouseRelease(mousePos.x, mousePos.y, mb, getMouseState(event));
368         else
369         {
370             // Fix Issue #281
371             // This resets _lastMouseX and _lastMouseY on new clicks,
372             // necessary if the focus was lost for a while.
373             _firstMouseMove = true;
374 
375             int clickCount = event.clickCount();
376             bool isDoubleClick = clickCount >= 2;
377             _listener.onMouseClick(mousePos.x, mousePos.y, mb, isDoubleClick, getMouseState(event));
378         }
379     }
380 
381     void layout()
382     {
383         // Updates internal buffers in case of startup/resize
384         {
385             NSRect frameRect = _view.frame();
386             // Note: even if the frame rect is wrong, we can support any internal size with cropping etc.
387             // TODO: is it really wrong though?
388             int width = cast(int)(frameRect.size.width);   // truncating down the dimensions of bounds
389             int height = cast(int)(frameRect.size.height);
390             updateSizeIfNeeded(width, height);
391         }
392     }
393 
394     void viewWillDraw()
395     {
396         if (_drawRectWorkaround)
397         {
398             CALayer layer = _view.layer();
399 
400             if (layer)
401             {
402                 // On Big Sur this is technically a no-op, but that reverts the drawRect behaviour!
403                 // This workaround is sanctionned by Apple: https://gist.github.com/lukaskubanek/9a61ac71dc0db8bb04db2028f2635779#gistcomment-3901461
404                 layer.setContentsFormat(kCAContentsFormatRGBA8Uint);
405             }
406         }
407     }
408 
409     void drawRect(NSRect rect)
410     {
411         NSGraphicsContext nsContext = NSGraphicsContext.currentContext();
412 
413         // The first drawRect callback occurs before the timer triggers.
414         // But because recomputeDirtyAreas() wasn't called before there is nothing to draw.
415         // Hence, do it.
416         if (_dirtyAreasAreNotYetComputed)
417         {
418             _dirtyAreasAreNotYetComputed = false;
419             _listener.recomputeDirtyAreas();
420         }
421 
422         _listener.onDraw(WindowPixelFormat.ARGB8);
423 
424         version(useCoreGraphicsContext)
425         {
426             CGContextRef cgContext = nsContext.getCGContext();
427 
428             enum bool fullDraw = false;
429 
430             static if (fullDraw)
431             {
432                 size_t sizeNeeded = _wfb.pitch * _wfb.h;
433                 size_t bytesPerRow = _wfb.pitch;
434 
435                 CGDataProviderRef provider = CGDataProviderCreateWithData(null, _wfb.pixels, sizeNeeded, null);
436                 CGImageRef image = CGImageCreate(_width,
437                                                  _height,
438                                                  8,
439                                                  32,
440                                                  bytesPerRow,
441                                                  _cgColorSpaceRef,
442                                                  kCGImageByteOrderDefault | kCGImageAlphaNoneSkipFirst,
443                                                  provider,
444                                                  null,
445                                                  true,
446                                                  kCGRenderingIntentDefault);
447                 // "on return, you may safely release [the provider]"
448                 CGDataProviderRelease(provider);
449                 scope(exit) CGImageRelease(image);
450 
451                 CGRect fullRect = CGMakeRect(0, 0, _width, _height);
452                 CGContextDrawImage(cgContext, fullRect, image);
453             }
454             else
455             {
456                 alias r = rect;
457 
458                 int origX = cast(int)rect.origin.x;
459                 int origY = cast(int)rect.origin.y;
460                 int width = cast(int)rect.size.width;
461                 int height = cast(int)rect.size.height;
462 
463                 int ysource = -origY + _height - height;
464 
465                 assert(ysource >= 0);
466                 assert(ysource < _height);
467 
468                 const (RGBA)* firstPixel = &(_wfb.scanline(ysource)[origX]);
469                 size_t sizeNeeded = _wfb.pitch * height;
470                 size_t bytesPerRow = _wfb.pitch;
471 
472                 CGDataProviderRef provider = CGDataProviderCreateWithData(null, firstPixel, sizeNeeded, null);
473 
474                 CGImageRef image = CGImageCreate(width,
475                                                  height,
476                                                  8,
477                                                  32,
478                                                  bytesPerRow,
479                                                  _cgColorSpaceRef,
480                                                  kCGImageByteOrderDefault | kCGImageAlphaNoneSkipFirst,
481                                                  provider,
482                                                  null,
483                                                  true,
484                                                  kCGRenderingIntentDefault);
485                 // "on return, you may safely release [the provider]"
486                 CGDataProviderRelease(provider);
487                 scope(exit) CGImageRelease(image);
488 
489                 //CGRect fullRect = CGMakeRect(0, 0, width, height);
490                 CGContextDrawImage(cgContext, rect, image);
491             }
492         }
493         else
494         {
495             size_t sizeNeeded = _wfb.pitch * _wfb.h;
496             size_t bytesPerRow = _wfb.pitch;
497             CIContext ciContext = nsContext.getCIContext();
498             _imageData = NSData.dataWithBytesNoCopy(_wfb.pixels, sizeNeeded, false);
499 
500             CIImage image = CIImage.imageWithBitmapData(_imageData,
501                                                         bytesPerRow,
502                                                         CGSize(_width, _height),
503                                                         kCIFormatARGB8,
504                                                         _cgColorSpaceRef);
505             ciContext.drawImage(image, rect, rect);
506         }
507     }
508 
509     /// Returns: true if window size changed.
510     bool updateSizeIfNeeded(int newWidth, int newHeight)
511     {
512         // only do something if the client size has changed
513         if ( (newWidth != _width) || (newHeight != _height) )
514         {
515             _width = newWidth;
516             _height = newHeight;
517             _wfb = _listener.onResized(_width, _height);
518             return true;
519         }
520         else
521             return false;
522     }
523 
524     void doAnimation()
525     {
526         uint now = getTimeMs();
527         double dt = (now - _lastMeasturedTimeInMs) * 0.001;
528         double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days
529         _lastMeasturedTimeInMs = now;
530         _listener.onAnimate(dt, time);
531     }
532 
533     void onTimer()
534     {
535         // Deal with animation
536         doAnimation();
537 
538         _listener.recomputeDirtyAreas();
539         _dirtyAreasAreNotYetComputed = false;
540         box2i dirtyRect = _listener.getDirtyRectangle();
541         if (!dirtyRect.empty())
542         {
543 
544             NSRect boundsRect = _view.bounds();
545             int height = cast(int)(boundsRect.size.height);
546             NSRect r = NSMakeRect(dirtyRect.min.x,
547                                     height - dirtyRect.min.y - dirtyRect.height,
548                                     dirtyRect.width,
549                                     dirtyRect.height);
550             _view.setNeedsDisplayInRect(r);
551         }
552     }
553 
554     void setMouseCursor(MouseCursor dplugCursor)
555     {
556         if(dplugCursor != _lastMouseCursor)
557         {
558             if(dplugCursor == MouseCursor.hidden)
559             {
560                 NSCursor.hide();
561             }
562             else
563             {
564                 if(_lastMouseCursor == MouseCursor.hidden)
565                 {
566                     NSCursor.unhide();
567                 }
568 
569                 NSCursor.pop();
570                 NSCursor nsCursor;
571                 switch(dplugCursor)
572                 {
573                     case MouseCursor.linkSelect:
574                         nsCursor = NSCursor.pointingHandCursor();
575                         break;
576                     case MouseCursor.drag:
577                         nsCursor = NSCursor.crosshairCursor();
578                         break;
579                     case MouseCursor.move:
580                         nsCursor = NSCursor.openHandCursor();
581                         break;
582                     case MouseCursor.verticalResize:
583                         nsCursor = NSCursor.resizeUpDownCursor();
584                         break;
585                     case MouseCursor.horizontalResize:
586                         nsCursor = NSCursor.resizeLeftRightCursor();
587                         break;
588                     case MouseCursor.diagonalResize:
589                         nsCursor = NSCursor.crosshairCursor(); // macOS doesn't seem to have this
590                         break;
591                     case MouseCursor.pointer:
592                     default:
593                         nsCursor = NSCursor.arrowCursor();
594                 }
595                 nsCursor.push();
596             }
597 
598             _lastMouseCursor = dplugCursor;
599         }
600     }
601 }
602 
603 struct DPlugCustomView
604 {
605 public:
606 nothrow:
607 @nogc:
608 
609     NSView parent;
610     alias parent this;
611 
612     // create from an id
613     this (id id_)
614     {
615         this._id = id_;
616     }
617 
618     /// Allocates, but do not init
619     static DPlugCustomView alloc()
620     {
621         alias fun_t = extern(C) id function (id obj, SEL sel) nothrow @nogc;
622         return DPlugCustomView( (cast(fun_t)objc_msgSend)(getClassID(), sel!"alloc") );
623     }
624 
625     static Class getClass()
626     {
627         return cast(Class)( getClassID() );
628     }
629 
630     static id getClassID()
631     {
632         return objc_getClass(customClassName.ptr);
633     }
634 
635     // This class uses a unique class name for each plugin instance
636     static __gshared char[16 + 36 + 1] customClassName;
637 
638     static void generateClassName() nothrow @nogc
639     {
640         generateNullTerminatedRandomUUID!char(customClassName, "DPlugCustomView_");
641     }
642 
643 private:
644 
645     CocoaWindow _window;
646     NSTimer _timer = null;
647     NSString _runLoopMode;
648     NSTrackingArea _trackingArea;
649 
650     void initialize(CocoaWindow window, int width, int height)
651     {
652         // Warning: taking this address is fishy since DPlugCustomView is a struct and thus could be copied
653         // we rely on the fact it won't :|
654         void* thisPointer = cast(void*)(&this);
655         object_setInstanceVariable(_id, "this", thisPointer);
656 
657         this._window = window;
658 
659         NSRect r = NSRect(NSPoint(0, 0), NSSize(width, height));
660         initWithFrame(r);
661 
662         _timer = NSTimer.timerWithTimeInterval(1 / 60.0, this, sel!"onTimer:", null, true);
663         _runLoopMode = NSString.stringWith("kCFRunLoopCommonModes"w);
664         NSRunLoop.currentRunLoop().addTimer(_timer, _runLoopMode);
665     }
666 
667     static __gshared Class clazz;
668 
669     static void registerSubclass()
670     {
671         clazz = objc_allocateClassPair(cast(Class) lazyClass!"NSView", customClassName.ptr, 0);
672 
673         class_addMethod(clazz, sel!"keyDown:", cast(IMP) &keyDown, "v@:@");
674         class_addMethod(clazz, sel!"keyUp:", cast(IMP) &keyUp, "v@:@");
675         class_addMethod(clazz, sel!"mouseDown:", cast(IMP) &mouseDown, "v@:@");
676         class_addMethod(clazz, sel!"mouseUp:", cast(IMP) &mouseUp, "v@:@");
677         class_addMethod(clazz, sel!"rightMouseDown:", cast(IMP) &rightMouseDown, "v@:@");
678         class_addMethod(clazz, sel!"rightMouseUp:", cast(IMP) &rightMouseUp, "v@:@");
679         class_addMethod(clazz, sel!"otherMouseDown:", cast(IMP) &otherMouseDown, "v@:@");
680         class_addMethod(clazz, sel!"otherMouseUp:", cast(IMP) &otherMouseUp, "v@:@");
681         class_addMethod(clazz, sel!"mouseMoved:", cast(IMP) &mouseMoved, "v@:@");
682         class_addMethod(clazz, sel!"mouseDragged:", cast(IMP) &mouseMoved, "v@:@");
683         class_addMethod(clazz, sel!"rightMouseDragged:", cast(IMP) &mouseMoved, "v@:@");
684         class_addMethod(clazz, sel!"otherMouseDragged:", cast(IMP) &mouseMoved, "v@:@");
685         class_addMethod(clazz, sel!"acceptsFirstResponder", cast(IMP) &acceptsFirstResponder, "b@:");
686         class_addMethod(clazz, sel!"isOpaque", cast(IMP) &isOpaque, "b@:");
687         class_addMethod(clazz, sel!"acceptsFirstMouse:", cast(IMP) &acceptsFirstMouse, "b@:@");
688         class_addMethod(clazz, sel!"viewDidMoveToWindow", cast(IMP) &viewDidMoveToWindow, "v@:");
689         class_addMethod(clazz, sel!"layout", cast(IMP) &layout, "v@:");
690         class_addMethod(clazz, sel!"drawRect:", cast(IMP) &drawRect, "v@:" ~ encode!NSRect);
691         class_addMethod(clazz, sel!"onTimer:", cast(IMP) &onTimer, "v@:@");
692         class_addMethod(clazz, sel!"viewWillDraw", cast(IMP) &viewWillDraw, "v@:");
693 
694         class_addMethod(clazz, sel!"mouseEntered:", cast(IMP) &mouseEntered, "v@:@");
695         class_addMethod(clazz, sel!"mouseExited:", cast(IMP) &mouseExited, "v@:@");
696         class_addMethod(clazz, sel!"updateTrackingAreas", cast(IMP)&updateTrackingAreas, "v@:");
697 
698         // This ~ is to avoid a strange DMD ICE. Didn't succeed in isolating it.
699         class_addMethod(clazz, sel!("scroll" ~ "Wheel:") , cast(IMP) &scrollWheel, "v@:@");
700 
701         // very important: add an instance variable for the this pointer so that the D object can be
702         // retrieved from an id
703         class_addIvar(clazz, "this", (void*).sizeof, (void*).sizeof == 4 ? 2 : 3, "^v");
704 
705         objc_registerClassPair(clazz);
706     }
707 
708     static void unregisterSubclass()
709     {
710         // For some reason the class need to continue to exist, so we leak it
711         //  objc_disposeClassPair(clazz);
712         // TODO: remove this crap
713     }
714 
715     void killTimer()
716     {
717         if (_timer)
718         {
719             _timer.invalidate();
720             _timer = NSTimer(null);
721         }
722     }
723 }
724 
725 DPlugCustomView getInstance(id anId) nothrow @nogc
726 {
727     // strange thing: object_getInstanceVariable definition is odd (void**)
728     // and only works for pointer-sized values says SO
729     void* thisPointer = null;
730     Ivar var = object_getInstanceVariable(anId, "this", &thisPointer);
731     assert(var !is null);
732     assert(thisPointer !is null);
733     return *cast(DPlugCustomView*)thisPointer;
734 }
735 
736 vec2i getMouseXY(NSView view, NSEvent event, int windowHeight) nothrow @nogc
737 {
738     NSPoint mouseLocation = event.locationInWindow();
739     mouseLocation = view.convertPoint(mouseLocation, NSView(null));
740     int px = cast(int)(mouseLocation.x) - 2;
741     int py = windowHeight - cast(int)(mouseLocation.y) - 3;
742     return vec2i(px, py);
743 }
744 
745 
746 
747 alias CocoaScopedCallback = ScopedForeignCallback!(true, true);
748 
749 // Overridden function gets called with an id, instead of the self pointer.
750 // So we have to get back the D class object address.
751 // Big thanks to Mike Ash (@macdev)
752 // MAYDO: why are these methods members???
753 extern(C)
754 {
755     void keyDown(id self, SEL selector, id event) nothrow @nogc
756     {
757         CocoaScopedCallback scopedCallback;
758         scopedCallback.enter();
759 
760         DPlugCustomView view = getInstance(self);
761         bool handled = view._window.handleKeyEvent(NSEvent(event), false);
762 
763         // send event to superclass if event not handled
764         if (!handled)
765         {
766             objc_super sup;
767             sup.receiver = self;
768             sup.clazz = cast(Class) lazyClass!"NSView";
769             alias fun_t = extern(C) void function (objc_super*, SEL, id) nothrow @nogc;
770             (cast(fun_t)objc_msgSendSuper)(&sup, selector, event);
771         }
772     }
773 
774     void keyUp(id self, SEL selector, id event) nothrow @nogc
775     {
776         CocoaScopedCallback scopedCallback;
777         scopedCallback.enter();
778 
779         DPlugCustomView view = getInstance(self);
780         view._window.handleKeyEvent(NSEvent(event), true);
781     }
782 
783     void mouseDown(id self, SEL selector, id event) nothrow @nogc
784     {
785         CocoaScopedCallback scopedCallback;
786         scopedCallback.enter();
787 
788         DPlugCustomView view = getInstance(self);
789         view._window.handleMouseClicks(NSEvent(event), MouseButton.left, false);
790     }
791 
792     void mouseUp(id self, SEL selector, id event) nothrow @nogc
793     {
794         CocoaScopedCallback scopedCallback;
795         scopedCallback.enter();
796 
797         DPlugCustomView view = getInstance(self);
798         view._window.handleMouseClicks(NSEvent(event), MouseButton.left, true);
799     }
800 
801     void rightMouseDown(id self, SEL selector, id event) nothrow @nogc
802     {
803         CocoaScopedCallback scopedCallback;
804         scopedCallback.enter();
805 
806         DPlugCustomView view = getInstance(self);
807         view._window.handleMouseClicks(NSEvent(event), MouseButton.right, false);
808     }
809 
810     void rightMouseUp(id self, SEL selector, id event) nothrow @nogc
811     {
812         CocoaScopedCallback scopedCallback;
813         scopedCallback.enter();
814 
815         DPlugCustomView view = getInstance(self);
816         view._window.handleMouseClicks(NSEvent(event), MouseButton.right, true);
817     }
818 
819     void otherMouseDown(id self, SEL selector, id event) nothrow @nogc
820     {
821         CocoaScopedCallback scopedCallback;
822         scopedCallback.enter();
823 
824         DPlugCustomView view = getInstance(self);
825         auto nsEvent = NSEvent(event);
826         if (nsEvent.buttonNumber == 2)
827             view._window.handleMouseClicks(nsEvent, MouseButton.middle, false);
828     }
829 
830     void otherMouseUp(id self, SEL selector, id event) nothrow @nogc
831     {
832         CocoaScopedCallback scopedCallback;
833         scopedCallback.enter();
834 
835         DPlugCustomView view = getInstance(self);
836         auto nsEvent = NSEvent(event);
837         if (nsEvent.buttonNumber == 2)
838             view._window.handleMouseClicks(nsEvent, MouseButton.middle, true);
839     }
840 
841     void mouseMoved(id self, SEL selector, id event) nothrow @nogc
842     {
843         CocoaScopedCallback scopedCallback;
844         scopedCallback.enter();
845 
846         DPlugCustomView view = getInstance(self);
847         view._window.handleMouseMove(NSEvent(event));
848     }
849 
850     void mouseEntered(id self, SEL selector, id event) nothrow @nogc
851     {
852         CocoaScopedCallback scopedCallback;
853         scopedCallback.enter();
854         DPlugCustomView view = getInstance(self);
855         view._window.handleMouseEntered(NSEvent(event));
856     }
857 
858     void mouseExited(id self, SEL selector, id event) nothrow @nogc
859     {
860         CocoaScopedCallback scopedCallback;
861         scopedCallback.enter();
862         DPlugCustomView view = getInstance(self);
863         view._window._listener.onMouseExitedWindow();
864     }
865 
866     void updateTrackingAreas(id self, SEL selector) nothrow @nogc
867     {
868         CocoaScopedCallback scopedCallback;
869         scopedCallback.enter();
870 
871         // Call superclass's updateTrackingAreas:, equivalent to [super updateTrackingAreas];
872         {
873             objc_super sup;
874             sup.receiver = self;
875             sup.clazz = cast(Class) lazyClass!"NSView";
876             alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc;
877             (cast(fun_t)objc_msgSendSuper)(&sup, selector);
878         }
879 
880         DPlugCustomView view = getInstance(self);
881 
882         // Remove an existing tracking area, if any.
883         if (view._trackingArea._id !is null)
884         {
885             view.removeTrackingArea(view._trackingArea);
886             view._trackingArea.release();
887             view._trackingArea._id = null;
888         }
889 
890         // This is needed to get mouseEntered and mouseExited
891         int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
892 
893         NSRect bounds = view.bounds();
894         view._trackingArea = NSTrackingArea.alloc();
895         view._trackingArea.initWithRect(bounds, opts, view, null);
896         view.addTrackingArea(view._trackingArea);
897     }
898 
899 
900     void scrollWheel(id self, SEL selector, id event) nothrow @nogc
901     {
902         CocoaScopedCallback scopedCallback;
903         scopedCallback.enter();
904         DPlugCustomView view = getInstance(self);
905         view._window.handleMouseWheel(NSEvent(event));
906     }
907 
908     bool acceptsFirstResponder(id self, SEL selector) nothrow @nogc
909     {
910         return YES;
911     }
912 
913     bool acceptsFirstMouse(id self, SEL selector, id pEvent) nothrow @nogc
914     {
915         return YES;
916     }
917 
918     bool isOpaque(id self, SEL selector) nothrow @nogc
919     {
920         return YES;
921     }
922 
923     // Since 10.7, called on resize.
924     void layout(id self, SEL selector) nothrow @nogc
925     {
926         CocoaScopedCallback scopedCallback;
927         scopedCallback.enter();
928 
929         DPlugCustomView view = getInstance(self);
930         view._window.layout();
931 
932         // Call superclass's layout:, equivalent to [super layout];
933         {
934             objc_super sup;
935             sup.receiver = self;
936             sup.clazz = cast(Class) lazyClass!"NSView";
937             alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc;
938             (cast(fun_t)objc_msgSendSuper)(&sup, selector);
939         }
940     }
941 
942     // Necessary for the Big Sur drawRect: fuckup
943     // See Issue #505.
944     void viewWillDraw(id self, SEL selector) nothrow @nogc
945     {
946         CocoaScopedCallback scopedCallback;
947         scopedCallback.enter();
948 
949         DPlugCustomView view = getInstance(self);
950         view._window.viewWillDraw();
951 
952         // Call superclass's layout:, equivalent to [super viewWillDraw];
953         {
954             objc_super sup;
955             sup.receiver = self;
956             sup.clazz = cast(Class) lazyClass!"NSView";
957             alias fun_t = extern(C) void function (objc_super*, SEL) nothrow @nogc;
958             (cast(fun_t)objc_msgSendSuper)(&sup, selector);
959         }
960     }
961 
962     void viewDidMoveToWindow(id self, SEL selector) nothrow @nogc
963     {
964         CocoaScopedCallback scopedCallback;
965         scopedCallback.enter();
966 
967         DPlugCustomView view = getInstance(self);
968         NSWindow parentWindow = view.window();
969         if (parentWindow)
970         {
971             parentWindow.makeFirstResponder(view);
972             parentWindow.setAcceptsMouseMovedEvents(true);
973         }
974     }
975 
976     void drawRect(id self, SEL selector, NSRect rect) nothrow @nogc
977     {
978         CocoaScopedCallback scopedCallback;
979         scopedCallback.enter();
980 
981         DPlugCustomView view = getInstance(self);
982         view._window.drawRect(rect);
983     }
984 
985     void onTimer(id self, SEL selector, id timer) nothrow @nogc
986     {
987         CocoaScopedCallback scopedCallback;
988         scopedCallback.enter();
989 
990         DPlugCustomView view = getInstance(self);
991         view._window.onTimer();
992     }
993 }