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 }