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 }