summaryrefslogtreecommitdiff
path: root/vcl/skia/osx/gdiimpl.cxx
blob: 7fa95e8d9fab7c383edf9c4ab5dad7d2e77683a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
/* -*- 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: */