1
0
Fork 0
libreoffice/vcl/skia/osx/gdiimpl.cxx
Daniel Baumann 8e63e14cf6
Adding upstream version 4:25.2.3.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 16:20:04 +02:00

423 lines
18 KiB
C++

/* -*- 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/.
*
* Some of this code is based on Skia source code, covered by the following
* license notice (see readlicense_oo for the full license):
*
* Copyright 2016 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
*/
#include <sal/config.h>
#include <skia/osx/gdiimpl.hxx>
#include <skia/utils.hxx>
#include <skia/zone.hxx>
//#include <tools/window/mac/WindowContextFactory_mac.h>
#include <quartz/CoreTextFont.hxx>
#include <quartz/SystemFontList.hxx>
#include <osx/salnativewidgets.h>
#include <skia/quartz/cgutils.h>
#include <tools/window/mac/MacWindowInfo.h>
#include <tools/window/mac/GaneshMetalWindowContext_mac.h>
#include <SkBitmap.h>
#include <SkCanvas.h>
#include <SkFont.h>
#include <SkFontMgr_mac_ct.h>
#include <SkTypeface_mac.h>
using namespace SkiaHelper;
namespace
{
struct SnapshotImageData
{
sk_sp<SkImage> image;
SkPixmap pixmap;
};
}
static void SnapshotImageDataCallback(void* pInfo, const void*, size_t)
{
if (pInfo)
delete static_cast<SnapshotImageData*>(pInfo);
}
AquaSkiaSalGraphicsImpl::AquaSkiaSalGraphicsImpl(AquaSalGraphics& rParent,
AquaSharedAttributes& rShared)
: SkiaSalGraphicsImpl(rParent, rShared.mpFrame)
, AquaGraphicsBackendBase(rShared, this)
{
}
AquaSkiaSalGraphicsImpl::~AquaSkiaSalGraphicsImpl()
{
DeInit(); // mac code doesn't call DeInit()
}
void AquaSkiaSalGraphicsImpl::createWindowSurfaceInternal(bool forceRaster)
{
assert(!mWindowContext);
assert(!mSurface);
SkiaZone zone;
skwindow::DisplayParams displayParams;
displayParams.fColorType = kN32_SkColorType;
skwindow::MacWindowInfo macWindow;
macWindow.fMainView = mrShared.mpFrame->mpNSView;
mScaling = getWindowScaling();
RenderMethod renderMethod = forceRaster ? RenderRaster : renderMethodToUse();
switch (renderMethod)
{
case RenderRaster:
// RasterWindowContext_mac uses OpenGL internally, which we don't want,
// so use our own surface and do blitting to the screen ourselves.
mSurface = createSkSurface(GetWidth() * mScaling, GetHeight() * mScaling);
break;
case RenderMetal:
mWindowContext = skwindow::MakeGaneshMetalForMac(macWindow, displayParams);
// Like with other GPU contexts, create a proxy offscreen surface (see
// flushSurfaceToWindowContext()). Here it's additionally needed because
// it appears that Metal surfaces cannot be read from, which would break things
// like copyArea().
if (mWindowContext)
mSurface = createSkSurface(GetWidth() * mScaling, GetHeight() * mScaling);
break;
case RenderVulkan:
abort();
break;
}
}
int AquaSkiaSalGraphicsImpl::getWindowScaling() const
{
// The system function returns float, but only integer multiples realistically make sense.
return sal::aqua::getWindowScaling();
}
void AquaSkiaSalGraphicsImpl::Flush() { performFlush(); }
void AquaSkiaSalGraphicsImpl::Flush(const tools::Rectangle&) { performFlush(); }
void AquaSkiaSalGraphicsImpl::WindowBackingPropertiesChanged() { windowBackingPropertiesChanged(); }
void AquaSkiaSalGraphicsImpl::flushSurfaceToWindowContext()
{
if (!isGPU())
{
// tdf159175 mark dirty area in NSWindow for redrawing
// This will cause -[SalFrameView drawRect:] to be called. That,
// in turn, will draw a CGImageRef of the surface fetched from
// AquaSkiaSalGraphicsImpl::createCGImageFromRasterSurface().
mrShared.refreshRect(mDirtyRect.x(), mDirtyRect.y(), mDirtyRect.width(),
mDirtyRect.height());
}
else
{
SkiaSalGraphicsImpl::flushSurfaceToWindowContext();
}
}
// For Raster we use our own screen blitting (see above).
CGImageRef AquaSkiaSalGraphicsImpl::createCGImageFromRasterSurface(const NSRect& rDirtyRect,
CGPoint& rImageOrigin,
bool& rImageFlipped)
{
if (isGPU() || !mSurface)
return nullptr;
// Based on AquaGraphicsBackend::drawBitmap().
if (!mrShared.checkContext())
return nullptr;
NSRect aIntegralRect = NSIntegralRect(rDirtyRect);
if (NSIsEmptyRect(aIntegralRect))
return nullptr;
// Do not use sub-rect, it creates copies of the data.
SnapshotImageData* pInfo = new SnapshotImageData;
pInfo->image = makeCheckedImageSnapshot(mSurface);
if (!pInfo->image->peekPixels(&pInfo->pixmap))
abort();
SkIRect aDirtyRect = SkIRect::MakeXYWH(
aIntegralRect.origin.x * mScaling, aIntegralRect.origin.y * mScaling,
aIntegralRect.size.width * mScaling, aIntegralRect.size.height * mScaling);
if (mrShared.isFlipped())
aDirtyRect = SkIRect::MakeXYWH(
aDirtyRect.x(), pInfo->pixmap.bounds().height() - aDirtyRect.y() - aDirtyRect.height(),
aDirtyRect.width(), aDirtyRect.height());
if (!aDirtyRect.intersect(pInfo->pixmap.bounds()))
{
delete pInfo;
return nullptr;
}
// If window scaling, then aDirtyRect is in scaled VCL coordinates and mSurface has
// screen size (=points,HiDPI).
// This creates the bitmap context from the cropped part, writable_addr32() will get
// the first pixel of aDirtyRect.topLeft(), and using pixmap.rowBytes() ensures the following
// pixel lines will be read from correct positions.
if (pInfo->pixmap.bounds() != aDirtyRect
&& pInfo->pixmap.bounds().bottom() == aDirtyRect.bottom())
{
// HACK for tdf#145843: If aDirtyRect includes the last line but not the first pixel of it,
// then the rowBytes() trick would lead to the CG* functions thinking that even pixels after
// the pixmap data belong to the area (since the shifted x()+rowBytes() points there) and
// at least on Intel Mac they would actually read those data, even though I see no good reason
// to do that, as that's beyond the x()+width() for the last line. That could be handled
// by creating a subset SkImage (which as is said above copies data), or set the x coordinate
// to 0, which will then make rowBytes() match the actual data.
aDirtyRect.fLeft = 0;
// Related tdf#156630 pixmaps can be wider than the dirty rectangle
// This seems to most commonly occur when SAL_FORCE_HIDPI_SCALING=1
// and the native window scale is 2.
assert(aDirtyRect.width() <= pInfo->pixmap.bounds().width());
}
// tdf#145843 Do not use CGBitmapContextCreate() to create a bitmap context
// As described in the comment in the above code, CGBitmapContextCreate()
// and CGBitmapContextCreateWithData() will try to access pixels up to
// aDirtyRect.x() + pixmap.bounds.width() for each row. When reading the
// last line in the SkPixmap, the buffer allocated for the SkPixmap ends at
// aDirtyRect.x() + aDirtyRect.width() and aDirtyRect.width() is clamped to
// pixmap.bounds.width() - aDirtyRect.x().
// This behavior looks like an optimization within CGBitmapContextCreate()
// to draw with a single memcpy() so fix this bug by chaining the
// CGDataProvider(), CGImageCreate(), and CGImageCreateWithImageInRect()
// functions to create the screen image.
CGDataProviderRef dataProvider
= CGDataProviderCreateWithData(pInfo, pInfo->pixmap.writable_addr32(0, 0),
pInfo->pixmap.computeByteSize(), SnapshotImageDataCallback);
if (!dataProvider)
{
delete pInfo;
SAL_WARN("vcl.skia", "flushSurfaceToScreenGC(): Failed to allocate data provider");
return nullptr;
}
CGImageRef fullImage
= CGImageCreate(pInfo->pixmap.bounds().width(), pInfo->pixmap.bounds().height(), 8,
8 * pInfo->image->imageInfo().bytesPerPixel(), pInfo->pixmap.rowBytes(),
GetSalData()->mxRGBSpace,
SkiaToCGBitmapType(pInfo->image->colorType(), pInfo->image->alphaType()),
dataProvider, nullptr, false, kCGRenderingIntentDefault);
if (!fullImage)
{
CGDataProviderRelease(dataProvider);
SAL_WARN("vcl.skia", "flushSurfaceToScreenGC(): Failed to allocate full image");
return nullptr;
}
CGImageRef screenImage = CGImageCreateWithImageInRect(
fullImage,
CGRectMake(aDirtyRect.x(), aDirtyRect.y(), aDirtyRect.width(), aDirtyRect.height()));
if (!screenImage)
{
CGImageRelease(fullImage);
CGDataProviderRelease(dataProvider);
SAL_WARN("vcl.skia", "createCGImageFromRasterSurface(): Failed to allocate screen image");
return nullptr;
}
rImageOrigin = CGPointMake(aDirtyRect.x(), aDirtyRect.y());
rImageFlipped = mrShared.isFlipped();
CGImageRelease(fullImage);
CGDataProviderRelease(dataProvider);
return screenImage;
}
bool AquaSkiaSalGraphicsImpl::drawNativeControl(ControlType nType, ControlPart nPart,
const tools::Rectangle& rControlRegion,
ControlState nState, const ImplControlValue& aValue)
{
// tdf#157613 make sure surface is not a nullptr
checkSurface();
if (!mSurface)
return false;
// rControlRegion is not the whole area that the control should be painted to (e.g. highlight
// around focused lineedit is outside of it). Since we draw to a temporary bitmap, we need to find out
// the real size.
// Related tdf#163945 Reduce the size of the temporary bitmap
// Previously, the temporary bitmap was set to the control region
// expanded by 50 * mScaling (e.g. both width and height were
// increased by 200 pixels when running on a Retina display). This
// caused temporary bitmaps to be up to several times larger than
// needed. Also, drawing NSBox objects to a CGBitmapContext is
// noticeably slow so filling all that unneeded temporary bitmap
// area can slow down performance when a large number of NSBox
// objects like the status bar are redrawn in quick succession.
// Using getNativeControlRegion() isn't perfect, but it does try to
// account for the focus ring as well as the minimum width and/or
// height of the native control so union the two regions set by
// getNativeControlRegion() and add double the focus ring width on
// each side just to be safe. In most cases, this should ensure
// that the temporary bitmap is large enough to draw the entire
// native control and a focus ring.
tools::Rectangle boundingRegion(rControlRegion);
tools::Rectangle rNativeBoundingRegion;
tools::Rectangle rNativeContentRegion;
AquaSalGraphics* pGraphics = mrShared.mpFrame->mpGraphics;
if (pGraphics
&& pGraphics->getNativeControlRegion(nType, nPart, rControlRegion, nState, aValue,
OUString(), rNativeBoundingRegion,
rNativeContentRegion))
{
boundingRegion.Union(rNativeBoundingRegion);
boundingRegion.Union(rNativeContentRegion);
}
boundingRegion.expand(2 * FOCUS_RING_WIDTH * mScaling);
// Do a scaled bitmap in HiDPI in order not to lose precision.
const tools::Long width = boundingRegion.GetWidth() * mScaling;
const tools::Long height = boundingRegion.GetHeight() * mScaling;
const size_t bytes = width * height * 4;
// Let Skia own the CGBitmapContext's buffer so that an SkImage
// can be created without Skia making a copy of the buffer
sk_sp<SkData> data = SkData::MakeZeroInitialized(bytes);
CGContextRef context = CGBitmapContextCreate(
data->writable_data(), width, height, 8, width * 4, GetSalData()->mxRGBSpace,
SkiaToCGBitmapType(mSurface->imageInfo().colorType(), kPremul_SkAlphaType));
if (!context)
{
SAL_WARN("vcl.skia", "drawNativeControl(): Failed to allocate bitmap context");
return false;
}
// Setup context state for drawing (performDrawNativeControl() e.g. fills background in some cases).
CGContextSetFillColorSpace(context, GetSalData()->mxRGBSpace);
CGContextSetStrokeColorSpace(context, GetSalData()->mxRGBSpace);
if (moLineColor)
{
RGBAColor lineColor(*moLineColor);
CGContextSetRGBStrokeColor(context, lineColor.GetRed(), lineColor.GetGreen(),
lineColor.GetBlue(), lineColor.GetAlpha());
}
if (moFillColor)
{
RGBAColor fillColor(*moFillColor);
CGContextSetRGBFillColor(context, fillColor.GetRed(), fillColor.GetGreen(),
fillColor.GetBlue(), fillColor.GetAlpha());
}
// Adjust for our drawn-to coordinates in the bitmap.
tools::Rectangle movedRegion(Point(rControlRegion.getX() - boundingRegion.getX(),
rControlRegion.getY() - boundingRegion.getY()),
rControlRegion.GetSize());
// Flip drawing upside down.
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1, -1);
// And possibly scale the native drawing.
CGContextScaleCTM(context, mScaling, mScaling);
bool bOK = performDrawNativeControl(nType, nPart, movedRegion, nState, aValue, context,
mrShared.mpFrame);
CGContextRelease(context);
if (bOK)
{
preDraw();
SAL_INFO("vcl.skia.trace", "drawnativecontrol(" << this << "): " << rControlRegion << ":"
<< int(nType) << "/" << int(nPart));
tools::Rectangle updateRect = boundingRegion;
// For background update only part that is not clipped, the same
// as in AquaGraphicsBackend::drawNativeControl().
if (nType == ControlType::WindowBackground)
updateRect.Intersection(mClipRegion.GetBoundRect());
addUpdateRegion(SkRect::MakeXYWH(updateRect.getX(), updateRect.getY(),
updateRect.GetWidth(), updateRect.GetHeight()));
SkRect drawRect = SkRect::MakeXYWH(boundingRegion.getX(), boundingRegion.getY(),
boundingRegion.GetWidth(), boundingRegion.GetHeight());
sk_sp<SkImage> image = SkImages::RasterFromData(
SkImageInfo::Make(width, height, mSurface->imageInfo().colorType(),
kPremul_SkAlphaType),
data, width * 4);
assert(image
&& drawRect.width() * mScaling == image->width()); // no scaling should be needed
getDrawCanvas()->drawImageRect(image, drawRect, SkSamplingOptions());
// Related: tdf#156881 flush the canvas after drawing the pixel buffer
if (auto dContext = GrAsDirectContext(getDrawCanvas()->recordingContext()))
dContext->flushAndSubmit();
++pendingOperationsToFlush; // tdf#136369
postDraw();
}
return bOK;
}
void AquaSkiaSalGraphicsImpl::drawTextLayout(const GenericSalLayout& rLayout)
{
const bool bSubpixelPositioning = rLayout.GetSubpixelPositioning();
const CoreTextFont& rFont = *static_cast<const CoreTextFont*>(&rLayout.GetFont());
const vcl::font::FontSelectPattern& rFontSelect = rFont.GetFontSelectPattern();
int nHeight = rFontSelect.mnHeight;
int nWidth = rFontSelect.mnWidth ? rFontSelect.mnWidth : nHeight;
if (nWidth == 0 || nHeight == 0)
{
SAL_WARN("vcl.skia", "DrawTextLayout(): rFontSelect.mnHeight is zero!?");
return;
}
if (!fontManager)
{
std::unique_ptr<SystemFontList> fontList = GetCoretextFontList();
if (fontList == nullptr)
{
SAL_WARN("vcl.skia", "DrawTextLayout(): No coretext font list");
fontManager = SkFontMgr_New_CoreText(nullptr);
}
else
{
fontManager = SkFontMgr_New_CoreText(fontList->fontCollection());
}
}
sk_sp<SkTypeface> typeface = SkMakeTypefaceFromCTFont(rFont.GetCTFont());
SkFont font(typeface);
font.setSize(nHeight);
// font.setScaleX(rFont.mfFontStretch); TODO
if (rFont.NeedsArtificialBold())
font.setEmbolden(true);
SkFont::Edging ePreferredAliasing
= bSubpixelPositioning ? SkFont::Edging::kSubpixelAntiAlias : SkFont::Edging::kAntiAlias;
if (bSubpixelPositioning)
{
// note that SkFont defaults to a BaselineSnap of true, so I think really only
// subpixel in text direction
font.setSubpixel(true);
}
font.setEdging(mrShared.mbNonAntialiasedText ? SkFont::Edging::kAlias : ePreferredAliasing);
// Vertical font, use width as "height".
SkFont verticalFont(font);
verticalFont.setSize(nHeight);
// verticalFont.setSize(nWidth); TODO
// verticalFont.setScaleX(1.0 * nHeight / nWidth);
drawGenericLayout(rLayout, mrShared.maTextColor, font, verticalFont);
}
namespace
{
std::unique_ptr<skwindow::WindowContext> createMetalWindowContext(bool /*temporary*/)
{
skwindow::DisplayParams displayParams;
skwindow::MacWindowInfo macWindow;
macWindow.fMainView = nullptr;
return skwindow::MakeGaneshMetalForMac(macWindow, displayParams);
}
}
void AquaSkiaSalGraphicsImpl::prepareSkia() { SkiaHelper::prepareSkia(createMetalWindowContext); }
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */