/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_FEATURE_SKIA #include #include #endif static bool bTotalScreenBounds = false; static NSRect aTotalScreenBounds = NSZeroRect; // TODO: Scale will be set to 2.0f as default after implementation of full scaled display support . This will allow moving of // windows between non retina and retina displays without blurry text and graphics. Static variables have to be removed thereafter. // Currently scaled display support is not implemented for bitmaps. This will cause a slight performance degradation on displays // with single precision. To preserve performance for now, window scaling is only activated if at least one display with double // precision is present. Moving windows between displays is then possible without blurry text and graphics too. Adapting window // scaling when displays are added while application is running is not supported. static bool bWindowScaling = false; static float fWindowScale = 1.0f; namespace sal::aqua { NSRect getTotalScreenBounds() { if (!bTotalScreenBounds) { aTotalScreenBounds = NSZeroRect; NSArray *aScreens = [NSScreen screens]; if (aScreens != nullptr) { for (NSScreen *aScreen : aScreens) { // Calculate total screen bounds NSRect aScreenFrame = [aScreen frame]; if (!NSIsEmptyRect(aScreenFrame)) { if (NSIsEmptyRect(aTotalScreenBounds)) aTotalScreenBounds = aScreenFrame; else aTotalScreenBounds = NSUnionRect( aScreenFrame, aTotalScreenBounds ); } } bTotalScreenBounds = true; } } return aTotalScreenBounds; } void resetTotalScreenBounds() { bTotalScreenBounds = false; getTotalScreenBounds(); } float getWindowScaling() { // Related: tdf#147342 Any changes to this function must be copied to the // sk_app::GetBackingScaleFactor() function in the following file: // workdir/UnpackedTarball/skia/tools/sk_app/mac/WindowContextFactory_mac.h if (!bWindowScaling) { NSArray *aScreens = [NSScreen screens]; if (aScreens != nullptr) { for (NSScreen *aScreen : aScreens) { float fScale = [aScreen backingScaleFactor]; if (fScale > fWindowScale) fWindowScale = fScale; } bWindowScaling = true; } if( const char* env = getenv("SAL_FORCE_HIDPI_SCALING")) { fWindowScale = atof(env); bWindowScaling = true; } } return fWindowScale; } void resetWindowScaling() { // Related: tdf#147342 Force recalculation of the window scaling but keep // the previous window scaling as the minimum so that we don't lose the // resolution in cached images if a HiDPI monitor is disconnected and // then reconnected. #if HAVE_FEATURE_SKIA if ( SkiaHelper::isVCLSkiaEnabled() ) sk_app::ResetBackingScaleFactor(); #endif bWindowScaling = false; getWindowScaling(); } } // end aqua void AquaSalGraphics::SetWindowGraphics( AquaSalFrame* pFrame ) { maShared.mpFrame = pFrame; maShared.mbWindow = true; maShared.mbPrinter = false; maShared.mbVirDev = false; mpBackend->UpdateGeometryProvider(pFrame); } void AquaSalGraphics::SetPrinterGraphics( CGContextRef xContext, sal_Int32 nDPIX, sal_Int32 nDPIY ) { maShared.mbWindow = false; maShared.mbPrinter = true; maShared.mbVirDev = false; maShared.maContextHolder.set(xContext); mnRealDPIX = nDPIX; mnRealDPIY = nDPIY; // a previously set clip path is now invalid maShared.unsetClipPath(); if (maShared.maContextHolder.isSet()) { CGContextSetFillColorSpace( maShared.maContextHolder.get(), GetSalData()->mxRGBSpace ); CGContextSetStrokeColorSpace( maShared.maContextHolder.get(), GetSalData()->mxRGBSpace ); CGContextSaveGState( maShared.maContextHolder.get() ); maShared.setState(); } mpBackend->UpdateGeometryProvider(nullptr); } void AquaSalGraphics::InvalidateContext() { UnsetState(); CGContextRelease(maShared.maContextHolder.get()); CGContextRelease(maShared.maBGContextHolder.get()); CGContextRelease(maShared.maCSContextHolder.get()); maShared.maContextHolder.set(nullptr); maShared.maCSContextHolder.set(nullptr); maShared.maBGContextHolder.set(nullptr); } void AquaSalGraphics::UnsetState() { if (maShared.maBGContextHolder.isSet()) { CGContextRelease(maShared.maBGContextHolder.get()); maShared.maBGContextHolder.set(nullptr); } if (maShared.maCSContextHolder.isSet()) { CGContextRelease(maShared.maCSContextHolder.get()); maShared.maBGContextHolder.set(nullptr); } if (maShared.maContextHolder.isSet()) { maShared.maContextHolder.restoreState(); maShared.maContextHolder.set(nullptr); } maShared.unsetState(); } /** * (re-)create the off-screen maLayer we render everything to if * necessary: eg. not initialized yet, or it has an incorrect size. */ bool AquaSharedAttributes::checkContext() { if (mbWindow && mpFrame && (mpFrame->getNSWindow() || Application::IsBitmapRendering())) { const unsigned int nWidth = mpFrame->maGeometry.width(); const unsigned int nHeight = mpFrame->maGeometry.height(); const float fScale = sal::aqua::getWindowScaling(); CGLayerRef rReleaseLayer = nullptr; // check if a new drawing context is needed (e.g. after a resize) if( (unsigned(mnWidth) != nWidth) || (unsigned(mnHeight) != nHeight) ) { mnWidth = nWidth; mnHeight = nHeight; // prepare to release the corresponding resources if (maLayer.isSet()) { rReleaseLayer = maLayer.get(); } else if (maContextHolder.isSet()) { CGContextRelease(maContextHolder.get()); } CGContextRelease(maBGContextHolder.get()); CGContextRelease(maCSContextHolder.get()); maContextHolder.set(nullptr); maBGContextHolder.set(nullptr); maCSContextHolder.set(nullptr); maLayer.set(nullptr); } // tdf#159175 no CGLayer is needed for an NSWindow when using Skia if (SkiaHelper::isVCLSkiaEnabled() && mpFrame->getNSWindow()) return true; if (!maContextHolder.isSet()) { const int nBitmapDepth = 32; float nScaledWidth = mnWidth * fScale; float nScaledHeight = mnHeight * fScale; const CGSize aLayerSize = { static_cast(nScaledWidth), static_cast(nScaledHeight) }; const int nBytesPerRow = (nBitmapDepth * nScaledWidth) / 8; std::uint32_t nFlags = std::uint32_t(kCGImageAlphaNoneSkipFirst) | std::uint32_t(kCGBitmapByteOrder32Host); maBGContextHolder.set(CGBitmapContextCreate( nullptr, nScaledWidth, nScaledHeight, 8, nBytesPerRow, GetSalData()->mxRGBSpace, nFlags)); maLayer.set(CGLayerCreateWithContext(maBGContextHolder.get(), aLayerSize, nullptr)); maLayer.setScale(fScale); nFlags = std::uint32_t(kCGImageAlphaPremultipliedFirst) | std::uint32_t(kCGBitmapByteOrder32Host); maCSContextHolder.set(CGBitmapContextCreate( nullptr, nScaledWidth, nScaledHeight, 8, nBytesPerRow, GetSalData()->mxRGBSpace, nFlags)); CGContextRef xDrawContext = CGLayerGetContext(maLayer.get()); maContextHolder = xDrawContext; if (rReleaseLayer) { // copy original layer to resized layer if (maContextHolder.isSet()) { CGContextDrawLayerAtPoint(maContextHolder.get(), CGPointZero, rReleaseLayer); } CGLayerRelease(rReleaseLayer); } if (maContextHolder.isSet()) { CGContextTranslateCTM(maContextHolder.get(), 0, nScaledHeight); CGContextScaleCTM(maContextHolder.get(), 1.0, -1.0); CGContextSetFillColorSpace(maContextHolder.get(), GetSalData()->mxRGBSpace); CGContextSetStrokeColorSpace(maContextHolder.get(), GetSalData()->mxRGBSpace); // apply a scale matrix so everything is auto-magically scaled CGContextScaleCTM(maContextHolder.get(), fScale, fScale); maContextHolder.saveState(); setState(); // re-enable XOR emulation for the new context if (mpXorEmulation) mpXorEmulation->SetTarget(mnWidth, mnHeight, mnBitmapDepth, maContextHolder.get(), maLayer.get()); } } } SAL_WARN_IF(!maContextHolder.isSet() && !mbPrinter, "vcl", "<<>> AquaSalGraphics::CheckContext() FAILED!!!!"); return maContextHolder.isSet(); } /** * Blit the contents of our internal maLayer state to the * associated window, if any; cf. drawRect event handling * on the frame. */ void AquaSalGraphics::UpdateWindow( NSRect& rRect ) { if (!maShared.mpFrame) { return; } NSGraphicsContext* pContext = [NSGraphicsContext currentContext]; if (!pContext) { SAL_WARN_IF(!maShared.mpFrame->mbInitShow, "vcl", "UpdateWindow called with no NSGraphicsContext"); return; } CGImageRef img = nullptr; CGPoint aImageOrigin = CGPointMake(0, 0); bool bImageFlipped = false; #if HAVE_FEATURE_SKIA if (SkiaHelper::isVCLSkiaEnabled()) { // tdf#159175 no CGLayer is needed for an NSWindow when using Skia // Get a CGImageRef directly from the Skia/Raster surface and draw // that directly to the NSWindow. // Note: Skia/Metal will always return a null CGImageRef since it // draws directly to the NSWindow using the surface's CAMetalLayer. AquaSkiaSalGraphicsImpl *pBackend = static_cast(mpBackend.get()); if (pBackend) img = pBackend->createCGImageFromRasterSurface(rRect, aImageOrigin, bImageFlipped); } else #else (void)rRect; #endif if (maShared.maLayer.isSet()) { maShared.applyXorContext(); const CGRect aRectPoints = { CGPointZero, maShared.maLayer.getSizePixels() }; CGContextSetBlendMode(maShared.maCSContextHolder.get(), kCGBlendModeCopy); CGContextDrawLayerInRect(maShared.maCSContextHolder.get(), aRectPoints, maShared.maLayer.get()); img = CGBitmapContextCreateImage(maShared.maCSContextHolder.get()); } if (img) { const float fScale = sal::aqua::getWindowScaling(); CGContextHolder rCGContextHolder([pContext CGContext]); rCGContextHolder.saveState(); CGRect aRect = CGRectMake(aImageOrigin.x / fScale, aImageOrigin.y / fScale, CGImageGetWidth(img) / fScale, CGImageGetHeight(img) / fScale); if (bImageFlipped) { // Related: tdf#155092 translate Y coordinate of flipped images // When in live resize, the NSView's height may have changed before // the surface has been resized. This causes flipped content // to be drawn just above or below the top left corner of the view // so translate the Y coordinate using the NSView's height. // Use the NSView's bounds, not its frame, to properly handle // any rotation and/or scaling that might have been already // applied to the view. NSView *pView = maShared.mpFrame->mpNSView; if (pView) aRect.origin.y = [pView bounds].size.height - aRect.origin.y - aRect.size.height; else if (maShared.maLayer.isSet()) aRect.origin.y = maShared.maLayer.getSizePoints().height - aRect.origin.y - aRect.size.height; } CGMutablePathRef rClip = maShared.mpFrame->getClipPath(); if (rClip) { CGContextBeginPath(rCGContextHolder.get()); CGContextAddPath(rCGContextHolder.get(), rClip ); CGContextClip(rCGContextHolder.get()); } CGContextSetBlendMode(rCGContextHolder.get(), kCGBlendModeCopy); // tdf#163152 don't convert image's sRGB colorspace // Converting the image's colorspace to match the window's // colorspace causes more than an expected amount of color // saturation so let the window's underlying CGContext handle // any necessary colorspace conversion in CGContextDrawImage(). CGContextDrawImage(rCGContextHolder.get(), aRect, img); rCGContextHolder.restoreState(); CGImageRelease(img); } else { SAL_WARN_IF(!maShared.mpFrame->mbInitShow, "vcl", "UpdateWindow called on uneligible graphics"); } } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */