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