1 /**
2 * Copyright: Copyright Auburn Sounds 2015 and later.
3 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
4 * Authors:   Guillaume Piolat
5 */
6 module dplug.window.carbonwindow;
7 
8 import core.stdc.stdio;
9 import core.stdc.stdlib;
10 
11 import std.string;
12 import std.math;
13 
14 import derelict.carbon;
15 
16 import gfm.math.vector;
17 import gfm.math.box;
18 import dplug.graphics.image;
19 import dplug.graphics.view;
20 
21 import dplug.core.runtime;
22 import dplug.core.nogc;
23 import dplug.window.window;
24 
25 
26 final class CarbonWindow : IWindow
27 {
28 nothrow:
29 @nogc:
30 private:
31     IWindowListener _listener;
32     bool _terminated = false;
33     bool _isComposited;
34     ControlRef _view = null;
35     WindowRef _window;
36     EventHandlerRef _controlHandler = null;
37     EventHandlerRef _windowHandler = null;
38     EventLoopTimerRef _timer = null;
39 
40     CGColorSpaceRef _colorSpace = null;
41     CGDataProviderRef _dataProvider = null;
42 
43     // Rendered frame buffer
44     ImageRef!RGBA _wfb;
45 
46     int _width = 0;
47     int _height = 0;
48     int _askedWidth;
49     int _askedHeight;
50     uint _timeAtCreationInMs;
51     uint _lastMeasturedTimeInMs;
52     long _ticksPerSecond;
53 
54     bool _dirtyAreasAreNotYetComputed = true; // TODO: could have a race on this if timer thread != draw thread
55     bool _firstMouseMove = true;
56 
57     int _lastMouseX;
58     int _lastMouseY;
59 
60 public:
61     this(void* parentWindow, void* parentControl, IWindowListener listener, int width, int height)
62     {
63         _ticksPerSecond = machTicksPerSecond();
64         _listener = listener;
65 
66         acquireCarbonFunctions();
67         acquireCoreFoundationFunctions();
68         acquireCoreServicesFunctions();
69         acquireCoreGraphicsFunctions();
70 
71         _askedWidth = width;
72         _askedHeight = height;
73 
74         _window = cast(WindowRef)(parentWindow);
75         WindowAttributes winAttrs = 0;
76         GetWindowAttributes(_window, &winAttrs);
77         _isComposited = (winAttrs & kWindowCompositingAttribute) != 0;
78 
79         UInt32 features =  kControlSupportsFocus | kControlHandlesTracking | kControlSupportsEmbedding;
80         if (_isComposited)
81             features |= kHIViewFeatureIsOpaque | kHIViewFeatureDoesNotUseSpecialParts;
82 
83         Rect r;
84         r.left = 0;
85         r.top = 0;
86         r.right = cast(short)width;
87         r.bottom = cast(short)height;
88 
89         CreateUserPaneControl(_window, &r, features, &_view);
90 
91         static immutable EventTypeSpec[] controlEvents =
92         [
93             EventTypeSpec(kEventClassControl, kEventControlDraw)
94         ];
95 
96         InstallControlEventHandler(_view, &eventCallback, controlEvents.length, controlEvents.ptr, cast(void*)this, &_controlHandler);
97 
98         static immutable EventTypeSpec[] windowEvents =
99         [
100             EventTypeSpec(kEventClassMouse, kEventMouseUp),
101             EventTypeSpec(kEventClassMouse, kEventMouseDown),
102             EventTypeSpec(kEventClassMouse, kEventMouseMoved),
103             EventTypeSpec(kEventClassMouse, kEventMouseDragged),
104             EventTypeSpec(kEventClassMouse, kEventMouseWheelMoved),
105             EventTypeSpec(kEventClassKeyboard, kEventRawKeyDown),
106             EventTypeSpec(kEventClassKeyboard, kEventRawKeyUp)
107         ];
108 
109         InstallWindowEventHandler(_window, &eventCallback, windowEvents.length, windowEvents.ptr, cast(void*)this, &_windowHandler);
110 
111         OSStatus s = InstallEventLoopTimer(GetMainEventLoop(), 0.0, kEventDurationSecond / 60.0,
112                                             &timerCallback, cast(void*)this, &_timer);
113 
114         // AU pass something, but VST does not.
115         ControlRef parentControlRef = cast(void*)parentControl;
116 
117         OSStatus status;
118         if (_isComposited)
119         {
120             if (!parentControlRef)
121             {
122                 HIViewRef hvRoot = HIViewGetRoot(_window);
123                 status = HIViewFindByID(hvRoot, kHIViewWindowContentID, &parentControlRef);
124             }
125 
126             status = HIViewAddSubview(parentControlRef, _view);
127         }
128         else
129         {
130             // MAYDO
131             /*if (!parentControlRef)
132             {
133                 if (GetRootControl(_window, &parentControlRef) != noErr)
134                 {
135                     CreateRootControl(_window, &parentControlRef);
136                 }
137             }
138             status = EmbedControl(_view, parentControlRef);
139             */
140             assert(false);
141         }
142 
143         if (status == noErr)
144             SizeControl(_view, r.right, r.bottom);  // offset?
145 
146         static immutable string colorSpaceName = "kCGColorSpaceSRGB";
147         CFStringRef str = CFStringCreateWithCString(null, colorSpaceName.ptr, kCFStringEncodingUTF8);
148         _colorSpace = CGColorSpaceCreateWithName(str);
149 
150         // TODO: release str which is leaking right now
151 
152         _lastMeasturedTimeInMs = _timeAtCreationInMs = getTimeMs();
153     }
154 
155     void clearDataProvider()
156     {
157         if (_dataProvider != null)
158         {
159             CGDataProviderRelease(_dataProvider);
160             _dataProvider = null;
161         }
162     }
163 
164     ~this()
165     {
166        _terminated = true;
167 
168         clearDataProvider();
169 
170         CGColorSpaceRelease(_colorSpace);
171 
172         RemoveEventLoopTimer(_timer);
173         RemoveEventHandler(_controlHandler);
174         RemoveEventHandler(_windowHandler);
175 
176         releaseCarbonFunctions();
177         releaseCoreFoundationFunctions();
178         releaseCoreServicesFunctions();
179         releaseCoreGraphicsFunctions();
180     }
181 
182 
183     // IWindow implmentation
184     override void waitEventAndDispatch()
185     {
186         assert(false); // Unimplemented, FUTURE
187     }
188 
189     // If exit was requested
190     override bool terminated()
191     {
192         return _terminated;
193     }
194 
195     override uint getTimeMs()
196     {
197         import core.time: convClockFreq;
198         long ticks = cast(long)mach_absolute_time();
199         long msecs = convClockFreq(ticks, _ticksPerSecond, 1_000);
200         return cast(uint)msecs;
201     }
202 
203     override void* systemHandle()
204     {
205         return _view;
206     }
207 
208 private:
209 
210     void doAnimation()
211     {
212         uint now = getTimeMs();
213         double dt = (now - _lastMeasturedTimeInMs) * 0.001;
214         double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days
215         _lastMeasturedTimeInMs = now;
216         _listener.onAnimate(dt, time);
217     }
218 
219     void onTimer()
220     {
221         // Deal with animation
222         doAnimation();
223 
224         _listener.recomputeDirtyAreas();
225         _dirtyAreasAreNotYetComputed = false;
226 
227         box2i dirtyRect = _listener.getDirtyRectangle();
228         if (!dirtyRect.empty())
229         {
230                 CGRect rect = CGRectMake(dirtyRect.min.x, dirtyRect.min.y, dirtyRect.width, dirtyRect.height);
231 
232                 // invalidate everything that is set dirty
233                 HIViewSetNeedsDisplayInRect(_view, &rect , true);
234         }
235     }
236 
237     vec2i getMouseXY(EventRef pEvent)
238     {
239         // Get mouse position
240         HIPoint mousePos;
241         GetEventParameter(pEvent, kEventParamWindowMouseLocation, typeHIPoint, null, HIPoint.sizeof, null, &mousePos);
242         HIPointConvert(&mousePos, kHICoordSpaceWindow, _window, kHICoordSpaceView, _view);
243         return vec2i(cast(int) round(mousePos.x - 2),
244                         cast(int) round(mousePos.y - 3) );
245     }
246 
247     MouseState getMouseState(EventRef pEvent)
248     {
249         UInt32 mods;
250         GetEventParameter(pEvent, kEventParamKeyModifiers, typeUInt32, null, UInt32.sizeof, null, &mods);
251 
252         MouseState state;
253         if (mods & btnState)
254             state.leftButtonDown = true;
255         if (mods & controlKey)
256             state.ctrlPressed = true;
257         if (mods & shiftKey)
258             state.shiftPressed = true;
259         if (mods & optionKey)
260             state.altPressed = true;
261 
262         return state;
263     }
264 
265     bool handleEvent(EventRef pEvent)
266     {
267         UInt32 eventClass = GetEventClass(pEvent);
268         UInt32 eventKind = GetEventKind(pEvent);
269 
270         switch(eventClass)
271         {
272             case kEventClassControl:
273             {
274                 switch(eventKind)
275                 {
276                     case kEventControlDraw:
277                     {
278                         // FUTURE: why is the bounds rect too large? It creates havoc in AU even without resizing.
279                         /*HIRect bounds;
280                         HIViewGetBounds(_view, &bounds);
281                         int newWidth = cast(int)(0.5f + bounds.size.width);
282                         int newHeight = cast(int)(0.5f + bounds.size.height);
283                         */
284                         int newWidth = _askedWidth; // In reaper, excess space is provided, leading in a crash
285                         int newHeight = _askedHeight; // fix size until we have resizeable UI
286                         updateSizeIfNeeded(newWidth, newHeight);
287 
288 
289                         if (_dirtyAreasAreNotYetComputed)
290                         {
291                             _dirtyAreasAreNotYetComputed = false;
292                             _listener.recomputeDirtyAreas();
293                         }
294 
295                         // Redraw dirty UI
296                         _listener.onDraw(WindowPixelFormat.RGBA8);
297 
298                         if (_isComposited)
299                         {
300                             CGContextRef contextRef;
301 
302                             // Get the CGContext
303                             GetEventParameter(pEvent, kEventParamCGContextRef, typeCGContextRef,
304                                                 null, CGContextRef.sizeof, null, &contextRef);
305 
306                             // Flip things vertically
307                             CGContextTranslateCTM(contextRef, 0, _height);
308                             CGContextScaleCTM(contextRef, 1.0f, -1.0f);
309 
310                             CGRect wholeRect = CGRect(CGPoint(0, 0), CGSize(_width, _height));
311 
312                             // See: http://stackoverflow.com/questions/2261177/cgimage-from-byte-array
313                             // Recreating this image looks necessary
314                             CGImageRef image = CGImageCreate(_width, _height, 8, 32, byteStride(_width), _colorSpace,
315                                                                 kCGBitmapByteOrderDefault, _dataProvider, null, false,
316                                                                 kCGRenderingIntentDefault);
317 
318                             CGContextDrawImage(contextRef, wholeRect, image);
319 
320                             CGImageRelease(image);
321                         }
322                         else
323                         {
324                             // MAYDO
325                         }
326                         return true;
327                     }
328 
329                     default:
330                         return false;
331                 }
332             }
333 
334             case kEventClassKeyboard:
335             {
336                 switch(eventKind)
337                 {
338                     case kEventRawKeyDown:
339                     case kEventRawKeyUp:
340                     {
341                         UInt32 k;
342                         GetEventParameter(pEvent, kEventParamKeyCode, typeUInt32, null, UInt32.sizeof, null, &k);
343 
344                         Key key;
345 
346                         bool handled = true;
347 
348                         switch(k)
349                         {
350                             case 125: key = Key.downArrow; break;
351                             case 126: key = Key.upArrow; break;
352                             case 123: key = Key.leftArrow; break;
353                             case 124: key = Key.rightArrow; break;
354                             case 0x35: key = Key.escape; break;
355                             case 0x24: key = Key.enter; break;
356                             case 0x52: key = Key.digit0; break;
357                             case 0x53: key = Key.digit1; break;
358                             case 0x54: key = Key.digit2; break;
359                             case 0x55: key = Key.digit3; break;
360                             case 0x56: key = Key.digit4; break;
361                             case 0x57: key = Key.digit5; break;
362                             case 0x58: key = Key.digit6; break;
363                             case 0x59: key = Key.digit7; break;
364                             case 0x5B: key = Key.digit8; break;
365                             case 0x5C: key = Key.digit9; break;
366                             default:
367                                 handled = false;
368                         }
369 
370                         if (handled)
371                         {
372                             if (eventKind == kEventRawKeyDown)
373                             {
374                                 if (!_listener.onKeyDown(key))
375                                     handled = false;
376                             }
377                             else
378                             {
379                                 if (!_listener.onKeyUp(key))
380                                     handled = false;
381                             }
382                         }
383                         return handled;
384                     }
385 
386                     default:
387                         return false;
388                 }
389             }
390 
391             case kEventClassMouse:
392             {
393                 switch(eventKind)
394                 {
395                     case kEventMouseUp:
396                     case kEventMouseDown:
397                     {
398                         vec2i mousePos = getMouseXY(pEvent);
399 
400                         // Get which button was pressed
401                         MouseButton mb;
402                         EventMouseButton button;
403                         GetEventParameter(pEvent, kEventParamMouseButton, typeMouseButton, null, EventMouseButton.sizeof, null, &button);
404                         switch(button)
405                         {
406                             case kEventMouseButtonPrimary:
407                                 mb = MouseButton.left;
408                                 break;
409                             case kEventMouseButtonSecondary:
410                                 mb = MouseButton.right;
411                                 break;
412                             case kEventMouseButtonTertiary:
413                                 mb = MouseButton.middle;
414                                 break;
415                             default:
416                                 return false;
417                         }
418 
419                         if (eventKind == kEventMouseDown)
420                         {
421                             UInt32 clickCount = 0;
422                             GetEventParameter(pEvent, kEventParamClickCount, typeUInt32, null, UInt32.sizeof, null, &clickCount);
423                             bool isDoubleClick = clickCount > 1;
424                             _listener.onMouseClick(mousePos.x, mousePos.y, mb, isDoubleClick, getMouseState(pEvent));
425                         }
426                         else
427                         {
428                             _listener.onMouseRelease(mousePos.x, mousePos.y, mb, getMouseState(pEvent));
429                         }
430                         return false;
431                     }
432 
433                     case kEventMouseMoved:
434                     case kEventMouseDragged:
435                     {
436                         vec2i mousePos = getMouseXY(pEvent);
437 
438                         if (_firstMouseMove)
439                         {
440                             _firstMouseMove = false;
441                             _lastMouseX = mousePos.x;
442                             _lastMouseY = mousePos.y;
443                         }
444 
445                         _listener.onMouseMove(mousePos.x, mousePos.y,
446                                                 mousePos.x - _lastMouseX, mousePos.y - _lastMouseY,
447                                                 getMouseState(pEvent));
448 
449                         _lastMouseX = mousePos.x;
450                         _lastMouseY = mousePos.y;
451                         return true;
452                     }
453 
454                     case kEventMouseWheelMoved:
455                     {
456                         EventMouseWheelAxis axis;
457                         GetEventParameter(pEvent, kEventParamMouseWheelAxis, typeMouseWheelAxis, null, EventMouseWheelAxis.sizeof, null, &axis);
458 
459                         if (axis == kEventMouseWheelAxisY)
460                         {
461                             int d;
462                             GetEventParameter(pEvent, kEventParamMouseWheelDelta, typeSInt32, null, SInt32.sizeof, null, &d);
463                             vec2i mousePos = getMouseXY(pEvent);
464                             _listener.onMouseWheel(mousePos.x, mousePos.y, 0, d, getMouseState(pEvent));
465                             return true;
466                         }
467 
468                         return false;
469                     }
470 
471                     default:
472                         return false;
473                 }
474             }
475 
476             default:
477                 return false;
478         }
479     }
480 
481     enum scanLineAlignment = 4; // could be anything
482 
483     // given a width, how long in bytes should scanlines be
484     int byteStride(int width)
485     {
486         int widthInBytes = width * 4;
487         return (widthInBytes + (scanLineAlignment - 1)) & ~(scanLineAlignment-1);
488     }
489 
490     /// Returns: true if window size changed.
491     bool updateSizeIfNeeded(int newWidth, int newHeight)
492     {
493         // only do something if the client size has changed
494         if ( (newWidth != _width) || (newHeight != _height) )
495         {
496             // Extends buffer
497             clearDataProvider();
498 
499             _width = newWidth;
500             _height = newHeight;
501             _wfb = _listener.onResized(_width, _height);
502 
503             // Create a new data provider
504             _dataProvider = CGDataProviderCreateWithData(null, _wfb.pixels, cast(int)(_wfb.pitch) * _wfb.h, null);
505             return true;
506         }
507         else
508             return false;
509     }
510 }
511 
512 alias CarbonScopedCallback = ScopedForeignCallback!(true, true);
513 
514 extern(C) OSStatus eventCallback(EventHandlerCallRef pHandlerCall, EventRef pEvent, void* user) nothrow @nogc
515 {
516     CarbonScopedCallback scopedCallback;
517     scopedCallback.enter();
518     CarbonWindow window = cast(CarbonWindow)user;
519     bool handled = window.handleEvent(pEvent);
520     return handled ? noErr : eventNotHandledErr;
521 }
522 
523 extern(C) void timerCallback(EventLoopTimerRef pTimer, void* user) nothrow @nogc
524 {
525     CarbonScopedCallback scopedCallback;
526     scopedCallback.enter();
527     CarbonWindow window = cast(CarbonWindow)user;
528     window.onTimer();
529 }
530 
531 
532 version(OSX)
533 {
534     extern(C) nothrow @nogc
535     {
536         struct mach_timebase_info_data_t
537         {
538             uint numer;
539             uint denom;
540         }
541         alias mach_timebase_info_data_t* mach_timebase_info_t;
542         alias kern_return_t = int;
543         kern_return_t mach_timebase_info(mach_timebase_info_t);
544         ulong mach_absolute_time();
545     }
546 
547     long machTicksPerSecond() nothrow @nogc
548     {
549         // Be optimistic that ticksPerSecond (1e9*denom/numer) is integral. So far
550         // so good on Darwin based platforms OS X, iOS.
551         import core.internal.abort : abort;
552         mach_timebase_info_data_t info;
553         if(mach_timebase_info(&info) != 0)
554             assert(false);
555 
556         long scaledDenom = 1_000_000_000L * info.denom;
557         if(scaledDenom % info.numer != 0)
558             assert(false);
559         return scaledDenom / info.numer;
560     }
561 }
562 else
563 {
564     ulong mach_absolute_time() nothrow @nogc
565     { 
566         return 0; 
567     }
568 
569     long machTicksPerSecond() nothrow @nogc
570     {
571         return 0;
572     }
573 }