// Missing features: // * The zoom button has been disabled to prevent instability from many types of partial full-screen that are currently not supported. // Potential optimizations: // * Let the MacOS compositor handle the up-scaling of pixels. // The compositor in MacOS is already rendering the uploaded canvas using bi-linear interpolation when the canvas is smaller than the window. // So changing the interpolation mode and adjusting the frame size to canvas size times pixel scale should give the same result. // It might reduce the amount of data sent to the compositor without depending directly on Metal. // CocoaWindow can then override a canvas upload from the original canvas resolution to reduce the amount of data sent to the compositor. // Then one can get thousands of frames per second, just like when changing the desktop resolution manually, but without getting blurry pixels from scaling with the wrong interpolation mode. // * Double buffering is disabled for safety by assigining bufferCount to 1 instead of 2 and copying presented pixel data to delayedCanvas. // Find a way to wait for the previous image to be displayed before giving Cocoa the next image, so that a full copy is not needed. #import #include "../DFPSR/api/imageAPI.h" #include "../DFPSR/api/drawAPI.h" #include "../DFPSR/api/timeAPI.h" #include "../DFPSR/implementation/gui/BackendWindow.h" #include "../DFPSR/base/heap.h" #include #include "../DFPSR/settings.h" static const int bufferCount = 1; static bool applicationInitialized = false; static NSApplication *application; class CocoaWindow : public dsr::BackendWindow { private: // Handle to the Cocoa window NSWindow *window = nullptr; NSView *view = nullptr; // The Core Graphics color space CGColorSpace *colorSpace = nullptr; // Identity to track enter and exit events for. SInt trackingNumber = 0; // Only accept non-drag move events when inside of the window. bool cursorInside = false; // Keeping track of control and command clicks. // 0 for regular left click. // 1 for control click converted to right mouse button. // 2 for command click converted to middle mouse button. int modifiedClick = 0; // Last modifiers to allow converting NSEventTypeFlagsChanged into up and down key press events. bool pressedControl = false; bool pressedCommand = false; bool pressedControlCommand = false; bool pressedShift = false; bool pressedAltOption = false; // Double buffering to allow drawing to a canvas while displaying the previous one // The image which can be drawn to, sharing memory with the Cocoa image dsr::AlignedImageRgbaU8 canvas[bufferCount]; // To prevent seeing unfinished scenes when rendering faster than the results can be displayed, a third canvas takes a copy from the finished image. dsr::Buffer delayedCanvas; // An Cocoa image wrapped around the canvas pixel data //NSImage *canvasNS[bufferCount] = {}; int drawIndex = 0 % bufferCount; int showIndex = 1 % bufferCount; // Remembers the dimensions of the window from creation and resize events // This allow requesting the size of the window at any time int windowWidth = 0, windowHeight = 0; // Called before the application fetches events from the input queue // Closing the window, moving the mouse, pressing a key, et cetera void prefetchEvents() override; // Called to change the cursor visibility and returning true on success bool setCursorVisibility(bool visible) override; // Place the cursor within the window bool setCursorPosition(int x, int y) override; private: // Helper methods specific to calling XLib void updateTitle(); private: // Canvas methods dsr::AlignedImageRgbaU8 getCanvas() override { return this->canvas[this->drawIndex]; } void resizeCanvas(int width, int height) override; // Window methods void setTitle(const dsr::String &newTitle) override { this->title = newTitle; this->updateTitle(); } int windowState = 0; // 0=none, 1=windowed, 2=fullscreen public: // Constructors CocoaWindow(const CocoaWindow&) = delete; // Non-copyable because of pointer aliasing. CocoaWindow(const dsr::String& title, int width, int height); int getWidth() const override { return this->windowWidth; }; int getHeight() const override { return this->windowHeight; }; // Destructor ~CocoaWindow(); // Full-screen void setFullScreen(bool enabled) override; bool isFullScreen() override { return this->windowState == 2; } // Showing the content void showCanvas() override; // Clipboard access dsr::ReadableString loadFromClipboard(double timeoutInSeconds) override; void saveToClipboard(const dsr::ReadableString &text, double timeoutInSeconds) override; }; static dsr::String nsToDsrString(const NSString *text) { dsr::String result; if (text != nullptr) { // Convert to a UTF-8 C string. const char *utf8text = [text cStringUsingEncoding:NSUTF8StringEncoding]; if (utf8text != nullptr) { // Convert to a DSR string. result = dsr::string_dangerous_decodeFromData(utf8text, dsr::CharacterEncoding::BOM_UTF8); } } return result; } static NSString *dsrToNsString(const dsr::ReadableString &text) { dsr::Buffer utf8buffer = dsr::string_saveToMemory(text, dsr::CharacterEncoding::BOM_UTF8, dsr::LineEncoding::Lf, false, true); char *utf8text = (char *)dsr::buffer_dangerous_getUnsafeData(utf8buffer); return [NSString stringWithUTF8String:utf8text]; } dsr::ReadableString CocoaWindow::loadFromClipboard(double timeoutInSeconds) { NSPasteboard *clipboard = [NSPasteboard generalPasteboard]; NSString *text = [clipboard stringForType:NSPasteboardTypeString]; if (text != nullptr) { return nsToDsrString(text); } else { return U""; } } void CocoaWindow::saveToClipboard(const dsr::ReadableString &text, double timeoutInSeconds) { NSString *savedText = dsrToNsString(text); NSPasteboard *clipboard = [NSPasteboard generalPasteboard]; [clipboard clearContents]; [clipboard setString:savedText forType:NSPasteboardTypeString]; } bool CocoaWindow::setCursorVisibility(bool visible) { if (visible) { [NSCursor unhide]; } else { [NSCursor hide]; } return true; } bool CocoaWindow::setCursorPosition(int x, int y) { // Get the offset from window pixels to screen pixels. NSWindow *window = [this->view window]; NSRect viewBounds = [this->view bounds]; NSRect viewRectInScreenCoords = [window convertRectToScreen:viewBounds]; CGPoint windowToScreenOffset = CGPointMake( viewRectInScreenCoords.origin.x, [[NSScreen mainScreen] frame].size.height - (viewRectInScreenCoords.origin.y + viewRectInScreenCoords.size.height) ); // Set the cursor position using screen coordinates. CGWarpMouseCursorPosition(CGPointMake(windowToScreenOffset.x + x, windowToScreenOffset.y + y)); // Prevent stalling after the move. CGAssociateMouseAndMouseCursorPosition(true); // TODO: How can the mouse move event be sent in the correct order in case of already having move events waiting? this->receivedMouseEvent(dsr::MouseEventType::MouseMove, dsr::MouseKeyEnum::NoKey, dsr::IVector2D(x, y)); return true; } void CocoaWindow::setFullScreen(bool enabled) { int newWindowState = enabled ? 2 : 1; if (newWindowState != this->windowState) { if (enabled) { // Entering full screen from the start or for an existing window. [this->view enterFullScreenMode:[NSScreen mainScreen] withOptions:nil]; this->windowState = 2; } else { if (this->windowState == 2) { // Leaving full screen instead of initializing a new window. [this->view exitFullScreenModeWithOptions:nil]; } this->windowState = 1; } } } void CocoaWindow::updateTitle() { // Get the title and convert it into the native string type. NSString *windowTitle = dsrToNsString(this->title); // Set the window title. [window setTitle:windowTitle]; } CocoaWindow::CocoaWindow(const dsr::String& title, int width, int height) { if (!applicationInitialized) { application = [NSApplication sharedApplication]; [application setActivationPolicy:NSApplicationActivationPolicyRegular]; [application setPresentationOptions:NSApplicationPresentationDefault]; [application activateIgnoringOtherApps:YES]; applicationInitialized = true; } bool fullScreen = false; if (width < 1 || height < 1) { fullScreen = true; width = 400; height = 300; } // Create a window @autoreleasepool { this->window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height) styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable backing: NSBackingStoreBuffered defer: NO]; } NSButton* zoomButton = [window standardWindowButton:NSWindowZoomButton]; [zoomButton setEnabled:NO]; // Get the view this->view = [window contentView]; this->setFullScreen(fullScreen); // Create a color space this->colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); if (this->colorSpace == nullptr) { dsr::throwError(U"Could not create a Core Graphics color space!\n"); } // Set the title this->setTitle(title); // Allocate a canvas this->resizeCanvas(width, height); // Show the window. [window center]; [window makeKeyAndOrderFront:nil]; [window makeFirstResponder:nil]; } static dsr::DsrKey getDsrKey(uint16_t keyCode) { dsr::DsrKey result = dsr::DsrKey_Unhandled; if (keyCode == 53) { result = dsr::DsrKey_Escape; } else if (keyCode == 122) { result = dsr::DsrKey_F1; } else if (keyCode == 120) { result = dsr::DsrKey_F2; } else if (keyCode == 99) { result = dsr::DsrKey_F3; } else if (keyCode == 118) { result = dsr::DsrKey_F4; } else if (keyCode == 96) { result = dsr::DsrKey_F5; } else if (keyCode == 97) { result = dsr::DsrKey_F6; } else if (keyCode == 98) { result = dsr::DsrKey_F7; } else if (keyCode == 100) { result = dsr::DsrKey_F8; } else if (keyCode == 101) { result = dsr::DsrKey_F9; } else if (keyCode == 109) { result = dsr::DsrKey_F10; } else if (keyCode == 103) { result = dsr::DsrKey_F11; } else if (keyCode == 111) { result = dsr::DsrKey_F12; } else if (keyCode == 105) { // F13 replaces the pause key that does not even have a keycode on MacOS. result = dsr::DsrKey_Pause; } else if (keyCode == 49) { result = dsr::DsrKey_Space; } else if (keyCode == 48) { result = dsr::DsrKey_Tab; } else if (keyCode == 36) { result = dsr::DsrKey_Return; } else if (keyCode == 51) { result = dsr::DsrKey_BackSpace; } else if (keyCode == 117) { result = dsr::DsrKey_Delete; } else if (keyCode == 123) { result = dsr::DsrKey_LeftArrow; } else if (keyCode == 124) { result = dsr::DsrKey_RightArrow; } else if (keyCode == 126) { result = dsr::DsrKey_UpArrow; } else if (keyCode == 125) { result = dsr::DsrKey_DownArrow; } else if (keyCode == 29) { result = dsr::DsrKey_0; } else if (keyCode == 18) { result = dsr::DsrKey_1; } else if (keyCode == 19) { result = dsr::DsrKey_2; } else if (keyCode == 20) { result = dsr::DsrKey_3; } else if (keyCode == 21) { result = dsr::DsrKey_4; } else if (keyCode == 23) { result = dsr::DsrKey_5; } else if (keyCode == 22) { result = dsr::DsrKey_6; } else if (keyCode == 26) { result = dsr::DsrKey_7; } else if (keyCode == 28) { result = dsr::DsrKey_8; } else if (keyCode == 25) { result = dsr::DsrKey_9; } else if (keyCode == 0) { result = dsr::DsrKey_A; } else if (keyCode == 11) { result = dsr::DsrKey_B; } else if (keyCode == 8) { result = dsr::DsrKey_C; } else if (keyCode == 2) { result = dsr::DsrKey_D; } else if (keyCode == 14) { result = dsr::DsrKey_E; } else if (keyCode == 3) { result = dsr::DsrKey_F; } else if (keyCode == 5) { result = dsr::DsrKey_G; } else if (keyCode == 4) { result = dsr::DsrKey_H; } else if (keyCode == 34) { result = dsr::DsrKey_I; } else if (keyCode == 38) { result = dsr::DsrKey_J; } else if (keyCode == 40) { result = dsr::DsrKey_K; } else if (keyCode == 37) { result = dsr::DsrKey_L; } else if (keyCode == 46) { result = dsr::DsrKey_M; } else if (keyCode == 45) { result = dsr::DsrKey_N; } else if (keyCode == 31) { result = dsr::DsrKey_O; } else if (keyCode == 35) { result = dsr::DsrKey_P; } else if (keyCode == 12) { result = dsr::DsrKey_Q; } else if (keyCode == 15) { result = dsr::DsrKey_R; } else if (keyCode == 1) { result = dsr::DsrKey_S; } else if (keyCode == 17) { result = dsr::DsrKey_T; } else if (keyCode == 32) { result = dsr::DsrKey_U; } else if (keyCode == 9) { result = dsr::DsrKey_V; } else if (keyCode == 13) { result = dsr::DsrKey_W; } else if (keyCode == 7) { result = dsr::DsrKey_X; } else if (keyCode == 16) { result = dsr::DsrKey_Y; } else if (keyCode == 6) { result = dsr::DsrKey_Z; } else if (keyCode == 114 || keyCode == 106) { // Insert on PC keyboard or F16 on Mac Keyboard. result = dsr::DsrKey_Insert; } else if (keyCode == 115) { result = dsr::DsrKey_Home; } else if (keyCode == 119) { result = dsr::DsrKey_End; } else if (keyCode == 116) { result = dsr::DsrKey_PageUp; } else if (keyCode == 121) { result = dsr::DsrKey_PageDown; } return result; } void CocoaWindow::prefetchEvents() { @autoreleasepool { CGFloat canvasWidth = NSWidth(this->view.bounds); CGFloat canvasHeight = NSHeight(this->view.bounds); // Process events while (true) { NSEvent *event = [application nextEventMatchingMask:NSEventMaskAny untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]; if (event == nullptr) break; if ([event type] == NSEventTypeLeftMouseDown || [event type] == NSEventTypeLeftMouseDragged || [event type] == NSEventTypeLeftMouseUp || [event type] == NSEventTypeRightMouseDown || [event type] == NSEventTypeRightMouseDragged || [event type] == NSEventTypeRightMouseUp || [event type] == NSEventTypeOtherMouseDown || [event type] == NSEventTypeOtherMouseDragged || [event type] == NSEventTypeOtherMouseUp || [event type] == NSEventTypeMouseMoved || [event type] == NSEventTypeMouseEntered || [event type] == NSEventTypeMouseExited || [event type] == NSEventTypeScrollWheel) { NSPoint point = [this->view convertPoint:[event locationInWindow] fromView:nil]; // This nasty hack combines an old mouse event with a canvas size that may have changed since the mouse event was created. // TODO: Find a way to get the canvas height from when the mouse event was actually created, so that lagging while resizing a window can not place click events at the wrong coordiates. dsr::IVector2D mousePosition = dsr::IVector2D(int32_t(point.x), int32_t(canvasHeight - point.y)); if ([event type] == NSEventTypeLeftMouseDown) { //dsr::printText(U"LeftMouseDown at ", mousePosition, U"\n"); this->cursorInside = true; // In case that enter events are missing, any proof of being inside of the window should be used. if (this->pressedControl) { // In case that control is released before the click is done, remember that the left click is a right click. this->modifiedClick = 1; this->receivedMouseEvent(dsr::MouseEventType::MouseDown, dsr::MouseKeyEnum::Right, mousePosition); } else if (this->pressedCommand) { // In case that control is released before the click is done, remember that the left click is a middle click. this->modifiedClick = 2; this->receivedMouseEvent(dsr::MouseEventType::MouseDown, dsr::MouseKeyEnum::Middle, mousePosition); } else { // Assume that the user only has one left mouse button, so that the state can be reset on each new left click. this->modifiedClick = 0; this->receivedMouseEvent(dsr::MouseEventType::MouseDown, dsr::MouseKeyEnum::Left, mousePosition); } } else if ([event type] == NSEventTypeLeftMouseDragged) { this->receivedMouseEvent(dsr::MouseEventType::MouseMove, dsr::MouseKeyEnum::NoKey, mousePosition); } else if ([event type] == NSEventTypeLeftMouseUp) { if (this->modifiedClick == 1) { // If the last left click was a control click, then the release should be treated as releasing the right mouse button. this->receivedMouseEvent(dsr::MouseEventType::MouseUp, dsr::MouseKeyEnum::Right, mousePosition); this->modifiedClick = 0; } else if (this->modifiedClick == 2) { // If the last left click was a command click, then the release should be treated as releasing the middle mouse button. this->receivedMouseEvent(dsr::MouseEventType::MouseUp, dsr::MouseKeyEnum::Middle, mousePosition); this->modifiedClick = 0; } else { this->receivedMouseEvent(dsr::MouseEventType::MouseUp, dsr::MouseKeyEnum::Left, mousePosition); } } else if ([event type] == NSEventTypeRightMouseDown) { this->cursorInside = true; // In case that enter events are missing, any proof of being inside of the window should be used. this->receivedMouseEvent(dsr::MouseEventType::MouseDown, dsr::MouseKeyEnum::Right, mousePosition); } else if ([event type] == NSEventTypeRightMouseDragged) { this->receivedMouseEvent(dsr::MouseEventType::MouseMove, dsr::MouseKeyEnum::NoKey, mousePosition); } else if ([event type] == NSEventTypeRightMouseUp) { this->receivedMouseEvent(dsr::MouseEventType::MouseUp, dsr::MouseKeyEnum::Right, mousePosition); } else if ([event type] == NSEventTypeOtherMouseDown) { this->cursorInside = true; // In case that enter events are missing, any proof of being inside of the window should be used. this->receivedMouseEvent(dsr::MouseEventType::MouseDown, dsr::MouseKeyEnum::Middle, mousePosition); } else if ([event type] == NSEventTypeOtherMouseDragged) { this->receivedMouseEvent(dsr::MouseEventType::MouseMove, dsr::MouseKeyEnum::NoKey, mousePosition); } else if ([event type] == NSEventTypeOtherMouseUp) { this->receivedMouseEvent(dsr::MouseEventType::MouseUp, dsr::MouseKeyEnum::Middle, mousePosition); } else if ([event type] == NSEventTypeMouseMoved) { // When not dragging, only allow move events inside of the view, to be consistent with other operating systems. if ((this->cursorInside || this->windowState == 2) && mousePosition.y >= 0) { this->receivedMouseEvent(dsr::MouseEventType::MouseMove, dsr::MouseKeyEnum::NoKey, mousePosition); } } else if ([event type] == NSEventTypeMouseEntered) { // TODO: This hack assumes that the first entering event goes to our view, but it would be more robust to get the tracking number directly from view. if (this->trackingNumber == 0) this->trackingNumber = event.trackingNumber; // Only accept enter events to our view. if (event.trackingNumber == this->trackingNumber) { this->cursorInside = true; } } else if ([event type] == NSEventTypeMouseExited) { // Only accept exit events from our view. if (event.trackingNumber == this->trackingNumber) { this->cursorInside = false; } } else if ([event type] == NSEventTypeScrollWheel) { if (event.scrollingDeltaY > 0.0) { this->receivedMouseEvent(dsr::MouseEventType::Scroll, dsr::MouseKeyEnum::ScrollUp, mousePosition); } if (event.scrollingDeltaY < 0.0) { this->receivedMouseEvent(dsr::MouseEventType::Scroll, dsr::MouseKeyEnum::ScrollDown, mousePosition); } } [application sendEvent:event]; } else if ([event type] == NSEventTypeKeyDown || [event type] == NSEventTypeKeyUp || [event type] == NSEventTypeFlagsChanged) { dsr::DsrKey code = getDsrKey(event.keyCode); if ([event type] == NSEventTypeKeyDown) { if (!(event.isARepeat)) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyDown, U'\0', code); } // Get typed characters if (event.characters != nullptr) { // Convert to a standard text format. const char *characters = [event.characters cStringUsingEncoding:NSUTF8StringEncoding]; if (characters != nullptr) { // Convert to a DSR string. dsr::String dsrCharacters = dsr::string_dangerous_decodeFromData(characters, dsr::CharacterEncoding::BOM_UTF8); // Send one type event for each character. for (intptr_t c = 0; c < string_length(dsrCharacters); c++) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyType, dsrCharacters[c], code); } } } } else if ([event type] == NSEventTypeKeyUp) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyUp, U'\0', code); } else if ([event type] == NSEventTypeFlagsChanged) { NSEventModifierFlags newModifierFlags = [event modifierFlags]; bool newControl = (newModifierFlags & NSEventModifierFlagControl) != 0u; bool newCommand = (newModifierFlags & NSEventModifierFlagCommand) != 0u; bool newControlCommand = (newModifierFlags & (NSEventModifierFlagControl | NSEventModifierFlagCommand)) != 0u; bool newShift = (newModifierFlags & NSEventModifierFlagShift) != 0u; bool newAltOption = (newModifierFlags & NSEventModifierFlagOption) != 0u; if (newControlCommand && !pressedControlCommand) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyDown, U'\0', dsr::DsrKey_Control); } else if (!newControlCommand && pressedControlCommand) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyUp, U'\0', dsr::DsrKey_Control); } if (newShift && !pressedShift) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyDown, U'\0', dsr::DsrKey_Shift); } else if (!newShift && pressedShift) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyUp, U'\0', dsr::DsrKey_Shift); } if (newAltOption && !pressedAltOption) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyDown, U'\0', dsr::DsrKey_Alt); } else if (!newAltOption && pressedAltOption) { this->receivedKeyboardEvent(dsr::KeyboardEventType::KeyUp, U'\0', dsr::DsrKey_Alt); } this->pressedControl = newControl; this->pressedCommand = newCommand; this->pressedControlCommand = newControlCommand; this->pressedShift = newShift; this->pressedAltOption = newAltOption; } // TODO: Make sure that this does not break anything important. // Supressing beeps by not forwarding key events to the system. } else { [application sendEvent:event]; } [application updateWindows]; } // Handle changes to the window. if (![window isMiniaturized]) { if ([window isVisible]) { // The window is still visible, so check if it needs to resize the canvas. int32_t wholeCanvasWidth = int32_t(canvasWidth); int32_t wholeCanvasHeight = int32_t(canvasHeight); if (this->windowWidth != wholeCanvasWidth || this->windowHeight != wholeCanvasHeight) { this->resizeCanvas(wholeCanvasWidth, wholeCanvasHeight); this->windowWidth = wholeCanvasWidth; this->windowHeight = wholeCanvasHeight; // Make a request to resize the canvas this->receivedWindowResize(wholeCanvasWidth, wholeCanvasHeight); } } else { // The window is no longer visible, so send a close event to the application. this->receivedWindowCloseEvent(); } } } } static const dsr::PackOrderIndex MacOSPackOrder = dsr::PackOrderIndex::ABGR; void CocoaWindow::resizeCanvas(int width, int height) { for (int b = 0; b < bufferCount; b++) { if (image_exists(this->canvas[b])) { if (image_getWidth(this->canvas[b]) == width && image_getHeight(this->canvas[b]) == height) { // The canvas already has the requested resolution. return; } else { // Preserve the pre-existing image. dsr::AlignedImageRgbaU8 newImage = image_create_RgbaU8_native(width, height, MacOSPackOrder); dsr::draw_copy(newImage, this->canvas[b]); this->canvas[b] = newImage; } } else { // Allocate a new image. this->canvas[b] = image_create_RgbaU8_native(width, height, MacOSPackOrder); } } } CocoaWindow::~CocoaWindow() { if (this->colorSpace != nullptr) { CGColorSpaceRelease(this->colorSpace); } [this->window close]; window = nullptr; } void CocoaWindow::showCanvas() { if (![window isMiniaturized]) { @autoreleasepool { this->drawIndex = (this->drawIndex + 1) % bufferCount; this->showIndex = (this->showIndex + 1) % bufferCount; this->prefetchEvents(); int displayIndex = this->showIndex; if (this->view != nullptr) { // Get image dimensions. int32_t width = dsr::image_getWidth(this->canvas[displayIndex]); int32_t height = dsr::image_getHeight(this->canvas[displayIndex]); int32_t stride = dsr::image_getStride(this->canvas[displayIndex]); // Make a deep clone of the finished image before it gets overwritten by another frame. this->delayedCanvas = dsr::buffer_clone(this->canvas[displayIndex].impl_buffer); uint8_t *pixelData = dsr::buffer_dangerous_getUnsafeData(this->delayedCanvas); CGDataProvider *provider = CGDataProviderCreateWithData(nullptr, pixelData, stride * height, nullptr); CGImage *image = CGImageCreate(width, height, 8, 32, stride, this->colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast, provider, nullptr, false, kCGRenderingIntentDefault); CGDataProviderRelease(provider); if (image == nullptr) { dsr::throwError(U"Could not create a Core Graphics image!\n"); return; } this->view.wantsLayer = YES; this->view.layer.contents = (__bridge id)image; CGImageRelease(image); } } } } dsr::Handle createBackendWindow(const dsr::String& title, int width, int height) { return dsr::handle_create(title, width, height); }