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 }