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