You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notepad-plus-plus/scintilla/cocoa/PlatCocoa.mm

2381 lines
73 KiB

/**
* Scintilla source code edit control
* @file PlatCocoa.mm - implementation of platform facilities on macOS/Cocoa
*
* Written by Mike Lischke
* Based on PlatMacOSX.cxx
* Based on work by Evan Jones (c) 2002 <ejones@uwaterloo.ca>
* Based on PlatGTK.cxx Copyright 1998-2002 by Neil Hodgson <neilh@scintilla.org>
* The License.txt file describes the conditions under which this software may be distributed.
*
* Copyright 2009 Sun Microsystems, Inc. All rights reserved.
* This file is dual licensed under LGPL v2.1 and the Scintilla license (http://www.scintilla.org/License.txt).
*/
#include <cstddef>
#include <cstdlib>
#include <cassert>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <stdexcept>
#include <string_view>
#include <vector>
#include <map>
#include <optional>
#include <functional>
#include <memory>
#include <numeric>
#import <Foundation/NSGeometry.h>
#import "ScintillaTypes.h"
#import "ScintillaMessages.h"
#import "ScintillaStructures.h"
#import "Debugging.h"
#import "Geometry.h"
#import "Platform.h"
#include "XPM.h"
#include "UniConversion.h"
#import "ScintillaView.h"
#import "ScintillaCocoa.h"
#import "PlatCocoa.h"
using namespace Scintilla;
using namespace Scintilla::Internal;
extern sptr_t scintilla_send_message(void *sci, unsigned int iMessage, uptr_t wParam, sptr_t lParam);
//--------------------------------------------------------------------------------------------------
/**
* Converts a Point as used by Scintilla to a Quartz-style CGPoint.
*/
inline CGPoint CGPointFromPoint(Scintilla::Internal::Point pt) {
return CGPointMake(pt.x, pt.y);
}
//--------------------------------------------------------------------------------------------------
/**
* Converts a PRectangle as used by Scintilla to standard Obj-C NSRect structure .
*/
NSRect PRectangleToNSRect(const PRectangle &rc) {
return NSMakeRect(rc.left, rc.top, rc.Width(), rc.Height());
}
//--------------------------------------------------------------------------------------------------
/**
* Converts an NSRect as used by the system to a native Scintilla rectangle.
*/
PRectangle NSRectToPRectangle(const NSRect &rc) {
return PRectangle(rc.origin.x, rc.origin.y, NSMaxX(rc), NSMaxY(rc));
}
//--------------------------------------------------------------------------------------------------
/**
* Converts a PRectangle as used by Scintilla to a Quartz-style rectangle.
*/
inline CGRect PRectangleToCGRect(PRectangle &rc) {
return CGRectMake(rc.left, rc.top, rc.Width(), rc.Height());
}
//--------------------------------------------------------------------------------------------------
/**
* Converts a PRectangle as used by Scintilla to a Quartz-style rectangle.
* Result is inset by strokeWidth / 2 so stroking does not go outside the rectangle.
*/
inline CGRect CGRectFromPRectangleInset(PRectangle rc, XYPOSITION strokeWidth) {
const XYPOSITION halfStroke = strokeWidth / 2.0f;
const CGRect rect = PRectangleToCGRect(rc);
return CGRectInset(rect, halfStroke, halfStroke);
}
//----------------- FontQuartz ---------------------------------------------------------------------
class FontQuartz : public Font {
public:
std::unique_ptr<QuartzTextStyle> style;
FontQuartz(const FontParameters &fp) {
style = std::make_unique<QuartzTextStyle>();
// Create the font with attributes
QuartzFont font(fp.faceName, strlen(fp.faceName), fp.size, fp.weight, fp.stretch, fp.italic);
CTFontRef fontRef = font.getFontID();
style->setFontRef(fontRef, fp.characterSet);
}
FontQuartz(const QuartzTextStyle *style_) {
style = std::make_unique<QuartzTextStyle>(style_);
}
};
//--------------------------------------------------------------------------------------------------
static QuartzTextStyle *TextStyleFromFont(const Font *f) noexcept {
if (f) {
const FontQuartz *pfq = dynamic_cast<const FontQuartz *>(f);
if (pfq) {
return pfq->style.get();
}
}
return nullptr;
}
//--------------------------------------------------------------------------------------------------
/**
* Creates a CTFontRef with the given properties.
*/
std::shared_ptr<Font> Font::Allocate(const FontParameters &fp) {
return std::make_shared<FontQuartz>(fp);
}
//--------------------------------------------------------------------------------------------------
// Bidirectional text support for Arabic and Hebrew.
namespace {
CFIndex IndexFromPosition(std::string_view text, size_t position) {
const std::string_view textUptoPosition = text.substr(0, position);
return UTF16Length(textUptoPosition);
}
// Handling representations and tabs
struct Blob {
XYPOSITION width;
Blob(XYPOSITION width_) : width(width_) {
}
};
static void BlobDealloc(void *refCon) {
Blob *blob = static_cast<Blob *>(refCon);
delete blob;
}
static CGFloat BlobGetWidth(void *refCon) {
Blob *blob = static_cast<Blob *>(refCon);
return blob->width;
}
class ScreenLineLayout : public IScreenLineLayout {
CTLineRef line = NULL;
const std::string text;
public:
ScreenLineLayout(const IScreenLine *screenLine);
~ScreenLineLayout();
// IScreenLineLayout implementation
size_t PositionFromX(XYPOSITION xDistance, bool charPosition) override;
XYPOSITION XFromPosition(size_t caretPosition) override;
std::vector<Interval> FindRangeIntervals(size_t start, size_t end) override;
};
ScreenLineLayout::ScreenLineLayout(const IScreenLine *screenLine) : text(screenLine->Text()) {
const UInt8 *puiBuffer = reinterpret_cast<const UInt8 *>(text.data());
std::string_view sv = text;
// Start with an empty mutable attributed string and add each character to it.
CFMutableAttributedStringRef mas = CFAttributedStringCreateMutable(NULL, 0);
for (size_t bp=0; bp<text.length();) {
const unsigned char uch = text[bp];
const int utf8Status = UTF8Classify(sv);
const unsigned int byteCount = utf8Status & UTF8MaskWidth;
XYPOSITION repWidth = screenLine->RepresentationWidth(bp);
if (uch == '\t') {
// Find the size up to the tab
NSMutableAttributedString *nas = (__bridge NSMutableAttributedString *)mas;
const NSSize sizeUpTo = [nas size];
const XYPOSITION nextTab = screenLine->TabPositionAfter(sizeUpTo.width);
repWidth = nextTab - sizeUpTo.width;
}
CFAttributedStringRef as = NULL;
if (repWidth > 0.0f) {
CTRunDelegateCallbacks callbacks = {
.version = kCTRunDelegateVersion1,
.dealloc = BlobDealloc,
.getWidth = BlobGetWidth
};
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callbacks, new Blob(repWidth));
NSMutableAttributedString *masBlob = [[NSMutableAttributedString alloc] initWithString:@"X"];
NSRange rangeX = NSMakeRange(0, 1);
[masBlob addAttribute: (NSString *)kCTRunDelegateAttributeName value: (__bridge id)runDelegate range:rangeX];
CFRelease(runDelegate);
as = (CFAttributedStringRef)CFBridgingRetain(masBlob);
} else {
CFStringRef piece = CFStringCreateWithBytes(NULL,
&puiBuffer[bp],
byteCount,
kCFStringEncodingUTF8,
false);
const QuartzTextStyle *qts = TextStyleFromFont(screenLine->FontOfPosition(bp));
CFMutableDictionaryRef pieceAttributes = qts->getCTStyle();
as = CFAttributedStringCreate(NULL, piece, pieceAttributes);
CFRelease(piece);
}
CFAttributedStringReplaceAttributedString(mas,
CFRangeMake(CFAttributedStringGetLength(mas), 0),
as);
bp += byteCount;
sv.remove_prefix(byteCount);
CFRelease(as);
}
line = CTLineCreateWithAttributedString(mas);
CFRelease(mas);
}
ScreenLineLayout::~ScreenLineLayout() {
CFRelease(line);
}
size_t ScreenLineLayout::PositionFromX(XYPOSITION xDistance, bool charPosition) {
if (!line) {
return 0;
}
const CGPoint ptDistance = CGPointMake(xDistance, 0);
const CFIndex offset = CTLineGetStringIndexForPosition(line, ptDistance);
if (offset == kCFNotFound) {
return 0;
}
// Convert back to UTF-8 positions
return UTF8PositionFromUTF16Position(text, offset);
}
XYPOSITION ScreenLineLayout::XFromPosition(size_t caretPosition) {
if (!line) {
return 0.0;
}
// Convert from UTF-8 position
const CFIndex caretIndex = IndexFromPosition(text, caretPosition);
const CGFloat distance = CTLineGetOffsetForStringIndex(line, caretIndex, nullptr);
return distance;
}
void AddToIntervalVector(std::vector<Interval> &vi, XYPOSITION left, XYPOSITION right) {
const Interval interval = {left, right};
if (vi.empty()) {
vi.push_back(interval);
} else {
Interval &last = vi.back();
if (std::abs(last.right-interval.left) < 0.01) {
// If new left is very close to previous right then extend last item
last.right = interval.right;
} else {
vi.push_back(interval);
}
}
}
std::vector<Interval> ScreenLineLayout::FindRangeIntervals(size_t start, size_t end) {
if (!line) {
return {};
}
std::vector<Interval> ret;
// Convert from UTF-8 position
const CFIndex startIndex = IndexFromPosition(text, start);
const CFIndex endIndex = IndexFromPosition(text, end);
CFArrayRef runs = CTLineGetGlyphRuns(line);
const CFIndex runCount = CFArrayGetCount(runs);
for (CFIndex run=0; run<runCount; run++) {
CTRunRef aRun = static_cast<CTRunRef>(CFArrayGetValueAtIndex(runs, run));
const CFIndex glyphCount = CTRunGetGlyphCount(aRun);
const CFRange rangeAll = CFRangeMake(0, glyphCount);
std::vector<CFIndex> indices(glyphCount);
CTRunGetStringIndices(aRun, rangeAll, indices.data());
std::vector<CGPoint> positions(glyphCount);
CTRunGetPositions(aRun, rangeAll, positions.data());
std::vector<CGSize> advances(glyphCount);
CTRunGetAdvances(aRun, rangeAll, advances.data());
for (CFIndex glyph=0; glyph<glyphCount; glyph++) {
const CFIndex glyphIndex = indices[glyph];
const XYPOSITION xPosition = positions[glyph].x;
const XYPOSITION width = advances[glyph].width;
if ((glyphIndex >= startIndex) && (glyphIndex < endIndex)) {
AddToIntervalVector(ret, xPosition, xPosition + width);
}
}
}
return ret;
}
// Helper for SurfaceImpl::MeasureWidths that examines the glyph runs in a layout
void GetPositions(CTLineRef line, std::vector<CGFloat> &positions) {
// Find the advances of the text
std::vector<CGFloat> lineAdvances(positions.size());
CFArrayRef runs = CTLineGetGlyphRuns(line);
const CFIndex runCount = CFArrayGetCount(runs);
for (CFIndex run=0; run<runCount; run++) {
CTRunRef aRun = static_cast<CTRunRef>(CFArrayGetValueAtIndex(runs, run));
const CFIndex glyphCount = CTRunGetGlyphCount(aRun);
const CFRange rangeAll = CFRangeMake(0, glyphCount);
std::vector<CFIndex> indices(glyphCount);
CTRunGetStringIndices(aRun, rangeAll, indices.data());
std::vector<CGSize> advances(glyphCount);
CTRunGetAdvances(aRun, rangeAll, advances.data());
for (CFIndex glyph=0; glyph<glyphCount; glyph++) {
const CFIndex glyphIndex = indices[glyph];
if (glyphIndex >= positions.size()) {
return;
}
lineAdvances[glyphIndex] = advances[glyph].width;
}
}
// Accumulate advances into positions
std::partial_sum(lineAdvances.begin(), lineAdvances.end(),
positions.begin(), std::plus<CGFloat>());
}
const Supports SupportsCocoa[] = {
Supports::LineDrawsFinal,
Supports::PixelDivisions,
Supports::FractionalStrokeWidth,
Supports::TranslucentStroke,
Supports::PixelModification,
Supports::ThreadSafeMeasureWidths,
};
}
//----------------- SurfaceImpl --------------------------------------------------------------------
SurfaceImpl::SurfaceImpl() {
gc = NULL;
bitmapData.reset(); // Release will try and delete bitmapData if != nullptr
bitmapWidth = 0;
bitmapHeight = 0;
Release();
}
SurfaceImpl::SurfaceImpl(const SurfaceImpl *surface, int width, int height) {
// Create a new bitmap context, along with the RAM for the bitmap itself
bitmapWidth = width;
bitmapHeight = height;
const int bitmapBytesPerRow = (width * BYTES_PER_PIXEL);
const int bitmapByteCount = (bitmapBytesPerRow * height);
// Create an RGB color space.
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL)
return;
// Create the bitmap.
bitmapData.reset(new uint8_t[bitmapByteCount]);
// create the context
gc = CGBitmapContextCreate(bitmapData.get(),
width,
height,
BITS_PER_COMPONENT,
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);
if (gc == NULL) {
// the context couldn't be created for some reason,
// and we have no use for the bitmap without the context
bitmapData.reset();
}
// the context retains the color space, so we can release it
CGColorSpaceRelease(colorSpace);
if (gc && bitmapData) {
// "Erase" to white.
CGContextClearRect(gc, CGRectMake(0, 0, width, height));
CGContextSetRGBFillColor(gc, 1.0, 1.0, 1.0, 1.0);
CGContextFillRect(gc, CGRectMake(0, 0, width, height));
}
mode = surface->mode;
}
//--------------------------------------------------------------------------------------------------
SurfaceImpl::~SurfaceImpl() {
Clear();
}
//--------------------------------------------------------------------------------------------------
bool SurfaceImpl::UnicodeMode() const noexcept {
return mode.codePage == SC_CP_UTF8;
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::Clear() {
if (bitmapData) {
bitmapData.reset();
// We only "own" the graphics context if we are a bitmap context
if (gc)
CGContextRelease(gc);
}
gc = NULL;
bitmapWidth = 0;
bitmapHeight = 0;
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::Release() noexcept {
Clear();
}
//--------------------------------------------------------------------------------------------------
bool SurfaceImpl::Initialised() {
// We are initalised if the graphics context is not null
return gc != NULL;// || port != NULL;
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::Init(WindowID) {
// To be able to draw, the surface must get a CGContext handle. We save the graphics port,
// then acquire/release the context on an as-need basis (see above).
// XXX Docs on QDBeginCGContext are light, a better way to do this would be good.
// AFAIK we should not hold onto a context retrieved this way, thus the need for
// acquire/release of the context.
Release();
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::Init(SurfaceID sid, WindowID) {
Release();
gc = static_cast<CGContextRef>(sid);
CGContextSetLineWidth(gc, 1.0);
}
std::unique_ptr<Surface> SurfaceImpl::AllocatePixMap(int width, int height) {
return std::make_unique<SurfaceImpl>(this, width, height);
}
std::unique_ptr<SurfaceImpl> SurfaceImpl::AllocatePixMapImplementation(int width, int height) {
return std::make_unique<SurfaceImpl>(this, width, height);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::SetMode(SurfaceMode mode_) {
mode = mode_;
}
//--------------------------------------------------------------------------------------------------
int SurfaceImpl::SupportsFeature(Supports feature) noexcept {
for (const Supports f : SupportsCocoa) {
if (f == feature)
return 1;
}
return 0;
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::FillColour(ColourRGBA fill) {
// Set the Fill color to match
CGContextSetRGBFillColor(gc,
fill.GetRedComponent(),
fill.GetGreenComponent(),
fill.GetBlueComponent(),
fill.GetAlphaComponent());
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::PenColourAlpha(ColourRGBA fore) {
// Set the Stroke color to match
CGContextSetRGBStrokeColor(gc,
fore.GetRedComponent(),
fore.GetGreenComponent(),
fore.GetBlueComponent(),
fore.GetAlphaComponent());
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::SetFillStroke(FillStroke fillStroke) {
FillColour(fillStroke.fill.colour);
PenColourAlpha(fillStroke.stroke.colour);
CGContextSetLineWidth(gc, fillStroke.stroke.width);
}
//--------------------------------------------------------------------------------------------------
CGImageRef SurfaceImpl::CreateImage() {
// For now, assume that CreateImage can only be called on PixMap surfaces.
if (!bitmapData)
return NULL;
CGContextFlush(gc);
// Create an RGB color space.
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL)
return NULL;
const int bitmapBytesPerRow = bitmapWidth * BYTES_PER_PIXEL;
const int bitmapByteCount = bitmapBytesPerRow * bitmapHeight;
// Make a copy of the bitmap data for the image creation and divorce it
// From the SurfaceImpl lifetime
CFDataRef dataRef = CFDataCreate(kCFAllocatorDefault, bitmapData.get(), bitmapByteCount);
// Create a data provider.
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData(dataRef);
CGImageRef image = NULL;
if (dataProvider != NULL) {
// Create the CGImage.
image = CGImageCreate(bitmapWidth,
bitmapHeight,
BITS_PER_COMPONENT,
BITS_PER_PIXEL,
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast,
dataProvider,
NULL,
0,
kCGRenderingIntentDefault);
}
// The image retains the color space, so we can release it.
CGColorSpaceRelease(colorSpace);
colorSpace = NULL;
// Done with the data provider.
CGDataProviderRelease(dataProvider);
dataProvider = NULL;
// Done with the data provider.
CFRelease(dataRef);
return image;
}
//--------------------------------------------------------------------------------------------------
/**
* Returns the vertical logical device resolution of the main monitor.
* This is no longer called.
* For Cocoa, all screens are treated as 72 DPI, even retina displays.
*/
int SurfaceImpl::LogPixelsY() {
return 72;
}
//--------------------------------------------------------------------------------------------------
/**
* Returns the number of device pixels per logical pixel.
* 1 for older displays and 2 for retina displays. Potentially 3 for some phones.
*/
int SurfaceImpl::PixelDivisions() {
if (gc) {
const CGSize szDevice = CGContextConvertSizeToDeviceSpace(gc, CGSizeMake(1.0, 1.0));
const int devicePixels = std::round(szDevice.width);
assert(devicePixels == 1 || devicePixels == 2);
return devicePixels;
}
return 1;
}
//--------------------------------------------------------------------------------------------------
/**
* Converts the logical font height in points into a device height.
* For Cocoa, points are always used for the result even on retina displays.
*/
int SurfaceImpl::DeviceHeightFont(int points) {
return points;
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::LineDraw(Point start, Point end, Stroke stroke) {
PenColourAlpha(stroke.colour);
CGContextSetLineWidth(gc, stroke.width);
CGContextBeginPath(gc);
CGContextMoveToPoint(gc, start.x, start.y);
CGContextAddLineToPoint(gc, end.x, end.y);
CGContextStrokePath(gc);
CGContextSetLineWidth(gc, 1.0f);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::PolyLine(const Point *pts, size_t npts, Stroke stroke) {
PLATFORM_ASSERT(gc && (npts > 1));
if (!gc || (npts <= 1)) {
return;
}
PenColourAlpha(stroke.colour);
CGContextSetLineWidth(gc, stroke.width);
CGContextBeginPath(gc);
CGContextMoveToPoint(gc, pts[0].x, pts[0].y);
for (size_t i = 1; i < npts; i++) {
CGContextAddLineToPoint(gc, pts[i].x, pts[i].y);
}
CGContextStrokePath(gc);
CGContextSetLineWidth(gc, 1.0f);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::Polygon(const Scintilla::Internal::Point *pts, size_t npts, FillStroke fillStroke) {
std::vector<CGPoint> points;
std::transform(pts, pts + npts, std::back_inserter(points), CGPointFromPoint);
CGContextBeginPath(gc);
SetFillStroke(fillStroke);
// Draw the polygon
CGContextAddLines(gc, points.data(), npts);
// Explicitly close the path, so it is closed for stroking AND filling (implicit close = filling only)
CGContextClosePath(gc);
CGContextDrawPath(gc, kCGPathFillStroke);
// Restore as not all paths set
CGContextSetLineWidth(gc, 1.0f);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::RectangleDraw(PRectangle rc, FillStroke fillStroke) {
if (!gc)
return;
CGContextBeginPath(gc);
SetFillStroke(fillStroke);
CGContextAddRect(gc, CGRectFromPRectangleInset(rc, fillStroke.stroke.width));
CGContextDrawPath(gc, kCGPathFillStroke);
// Restore as not all paths set
CGContextSetLineWidth(gc, 1.0f);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::RectangleFrame(PRectangle rc, Stroke stroke) {
if (!gc)
return;
CGContextBeginPath(gc);
PenColourAlpha(stroke.colour);
CGContextSetLineWidth(gc, stroke.width);
CGContextAddRect(gc, CGRectFromPRectangleInset(rc, stroke.width));
CGContextDrawPath(gc, kCGPathStroke);
// Restore as not all paths set
CGContextSetLineWidth(gc, 1.0f);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::FillRectangle(PRectangle rc, Fill fill) {
if (gc) {
FillColour(fill.colour);
CGRect rect = PRectangleToCGRect(rc);
CGContextFillRect(gc, rect);
}
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::FillRectangleAligned(PRectangle rc, Fill fill) {
FillRectangle(PixelAlign(rc, PixelDivisions()), fill);
}
//--------------------------------------------------------------------------------------------------
static void drawImageRefCallback(void *info, CGContextRef gc) {
CGImageRef pattern = static_cast<CGImageRef>(info);
CGContextDrawImage(gc, CGRectMake(0, 0, CGImageGetWidth(pattern), CGImageGetHeight(pattern)), pattern);
}
//--------------------------------------------------------------------------------------------------
static void releaseImageRefCallback(void *info) {
CGImageRelease(static_cast<CGImageRef>(info));
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::FillRectangle(PRectangle rc, Surface &surfacePattern) {
SurfaceImpl &patternSurface = static_cast<SurfaceImpl &>(surfacePattern);
// For now, assume that copy can only be called on PixMap surfaces. Shows up black.
CGImageRef image = patternSurface.CreateImage();
if (image == NULL) {
FillRectangle(rc, ColourRGBA::FromRGB(0));
return;
}
const CGPatternCallbacks drawImageCallbacks = { 0, drawImageRefCallback, releaseImageRefCallback };
CGPatternRef pattern = CGPatternCreate(image,
CGRectMake(0, 0, patternSurface.bitmapWidth, patternSurface.bitmapHeight),
CGAffineTransformIdentity,
patternSurface.bitmapWidth,
patternSurface.bitmapHeight,
kCGPatternTilingNoDistortion,
true,
&drawImageCallbacks
);
if (pattern != NULL) {
// Create a pattern color space
CGColorSpaceRef colorSpace = CGColorSpaceCreatePattern(NULL);
if (colorSpace != NULL) {
CGContextSaveGState(gc);
CGContextSetFillColorSpace(gc, colorSpace);
// Unlike the documentation, you MUST pass in a "components" parameter:
// For coloured patterns it is the alpha value.
const CGFloat alpha = 1.0;
CGContextSetFillPattern(gc, pattern, &alpha);
CGContextFillRect(gc, PRectangleToCGRect(rc));
CGContextRestoreGState(gc);
// Free the color space, the pattern and image
CGColorSpaceRelease(colorSpace);
} /* colorSpace != NULL */
colorSpace = NULL;
CGPatternRelease(pattern);
pattern = NULL;
} /* pattern != NULL */
}
void SurfaceImpl::RoundedRectangle(PRectangle rc, FillStroke fillStroke) {
// This is only called from the margin marker drawing code for MarkerSymbol::RoundRect
// The Win32 version does
// ::RoundRect(hdc, rc.left + 1, rc.top, rc.right - 1, rc.bottom, 8, 8 );
// which is a rectangle with rounded corners each having a radius of 4 pixels.
// It would be almost as good just cutting off the corners with lines at
// 45 degrees as is done on GTK+.
// Create a rectangle with semicircles at the corners
const int MAX_RADIUS = 4;
const int radius = std::min(MAX_RADIUS, static_cast<int>(std::min(rc.Height()/2, rc.Width()/2)));
// Points go clockwise, starting from just below the top left
// Corners are kept together, so we can easily create arcs to connect them
CGPoint corners[4][3] = {
{
{ rc.left, rc.top + radius },
{ rc.left, rc.top },
{ rc.left + radius, rc.top },
},
{
{ rc.right - radius - 1, rc.top },
{ rc.right - 1, rc.top },
{ rc.right - 1, rc.top + radius },
},
{
{ rc.right - 1, rc.bottom - radius - 1 },
{ rc.right - 1, rc.bottom - 1 },
{ rc.right - radius - 1, rc.bottom - 1 },
},
{
{ rc.left + radius, rc.bottom - 1 },
{ rc.left, rc.bottom - 1 },
{ rc.left, rc.bottom - radius - 1 },
},
};
// Align the points in the middle of the pixels
for (int i = 0; i < 4; ++ i) {
for (int j = 0; j < 3; ++ j) {
corners[i][j].x += 0.5;
corners[i][j].y += 0.5;
}
}
SetFillStroke(fillStroke);
// Move to the last point to begin the path
CGContextBeginPath(gc);
CGContextMoveToPoint(gc, corners[3][2].x, corners[3][2].y);
for (int i = 0; i < 4; ++ i) {
CGContextAddLineToPoint(gc, corners[i][0].x, corners[i][0].y);
CGContextAddArcToPoint(gc, corners[i][1].x, corners[i][1].y, corners[i][2].x, corners[i][2].y, radius);
}
// Close the path to enclose it for stroking and for filling, then draw it
CGContextClosePath(gc);
CGContextDrawPath(gc, kCGPathFillStroke);
// Restore as not all paths set
CGContextSetLineWidth(gc, 1.0f);
}
// DrawChamferedRectangle is a helper function for AlphaRectangle that either fills or strokes a
// rectangle with its corners chamfered at 45 degrees.
static void DrawChamferedRectangle(CGContextRef gc, PRectangle rc, int cornerSize, CGPathDrawingMode mode) {
// Points go clockwise, starting from just below the top left
CGPoint corners[4][2] = {
{
{ rc.left, rc.top + cornerSize },
{ rc.left + cornerSize, rc.top },
},
{
{ rc.right - cornerSize - 1, rc.top },
{ rc.right - 1, rc.top + cornerSize },
},
{
{ rc.right - 1, rc.bottom - cornerSize - 1 },
{ rc.right - cornerSize - 1, rc.bottom - 1 },
},
{
{ rc.left + cornerSize, rc.bottom - 1 },
{ rc.left, rc.bottom - cornerSize - 1 },
},
};
// Align the points in the middle of the pixels
for (int i = 0; i < 4; ++ i) {
for (int j = 0; j < 2; ++ j) {
corners[i][j].x += 0.5;
corners[i][j].y += 0.5;
}
}
// Move to the last point to begin the path
CGContextBeginPath(gc);
CGContextMoveToPoint(gc, corners[3][1].x, corners[3][1].y);
for (int i = 0; i < 4; ++ i) {
CGContextAddLineToPoint(gc, corners[i][0].x, corners[i][0].y);
CGContextAddLineToPoint(gc, corners[i][1].x, corners[i][1].y);
}
// Close the path to enclose it for stroking and for filling, then draw it
CGContextClosePath(gc);
CGContextDrawPath(gc, mode);
}
void Scintilla::Internal::SurfaceImpl::AlphaRectangle(PRectangle rc, XYPOSITION cornerSize, FillStroke fillStroke) {
if (gc) {
const XYPOSITION halfStroke = fillStroke.stroke.width / 2.0f;
// Set the Fill color to match
FillColour(fillStroke.fill.colour);
PenColourAlpha(fillStroke.stroke.colour);
PRectangle rcFill = rc;
if (cornerSize == 0) {
// A simple rectangle, no rounded corners
if (fillStroke.fill.colour == fillStroke.stroke.colour) {
// Optimization for simple case
CGRect rect = PRectangleToCGRect(rcFill);
CGContextFillRect(gc, rect);
} else {
rcFill.left += fillStroke.stroke.width;
rcFill.top += fillStroke.stroke.width;
rcFill.right -= fillStroke.stroke.width;
rcFill.bottom -= fillStroke.stroke.width;
CGRect rect = PRectangleToCGRect(rcFill);
CGContextFillRect(gc, rect);
CGContextAddRect(gc, CGRectMake(rc.left + halfStroke, rc.top + halfStroke,
rc.Width() - fillStroke.stroke.width, rc.Height() - fillStroke.stroke.width));
CGContextStrokeRectWithWidth(gc,
CGRectMake(rc.left + halfStroke, rc.top + halfStroke,
rc.Width() - fillStroke.stroke.width, rc.Height() - fillStroke.stroke.width),
fillStroke.stroke.width);
}
} else {
// Approximate rounded corners with 45 degree chamfers.
// Drawing real circular arcs often leaves some over- or under-drawn pixels.
if (fillStroke.fill.colour == fillStroke.stroke.colour) {
// Specializing this case avoids a few stray light/dark pixels in corners.
rcFill.left -= halfStroke;
rcFill.top -= halfStroke;
rcFill.right += halfStroke;
rcFill.bottom += halfStroke;
DrawChamferedRectangle(gc, rcFill, cornerSize, kCGPathFill);
} else {
rcFill.left += halfStroke;
rcFill.top += halfStroke;
rcFill.right -= halfStroke;
rcFill.bottom -= halfStroke;
DrawChamferedRectangle(gc, rcFill, cornerSize-fillStroke.stroke.width, kCGPathFill);
DrawChamferedRectangle(gc, rc, cornerSize, kCGPathStroke);
}
}
}
}
void Scintilla::Internal::SurfaceImpl::GradientRectangle(PRectangle rc, const std::vector<ColourStop> &stops, GradientOptions options) {
if (!gc) {
return;
}
CGPoint ptStart = CGPointMake(rc.left, rc.top);
CGPoint ptEnd = CGPointMake(rc.left, rc.bottom);
if (options == GradientOptions::leftToRight) {
ptEnd = CGPointMake(rc.right, rc.top);
}
std::vector<CGFloat> components;
std::vector<CGFloat> locations;
for (const ColourStop &stop : stops) {
locations.push_back(stop.position);
components.push_back(stop.colour.GetRedComponent());
components.push_back(stop.colour.GetGreenComponent());
components.push_back(stop.colour.GetBlueComponent());
components.push_back(stop.colour.GetAlphaComponent());
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (!colorSpace) {
return;
}
CGGradientRef gradiantRef = CGGradientCreateWithColorComponents(colorSpace,
components.data(),
locations.data(),
locations.size());
if (gradiantRef) {
CGContextSaveGState(gc);
CGRect rect = PRectangleToCGRect(rc);
CGContextClipToRect(gc, rect);
CGContextBeginPath(gc);
CGContextAddRect(gc, rect);
CGContextClosePath(gc);
CGContextDrawLinearGradient(gc, gradiantRef, ptStart, ptEnd, 0);
CGGradientRelease(gradiantRef);
CGContextRestoreGState(gc);
}
CGColorSpaceRelease(colorSpace);
}
static void ProviderReleaseData(void *, const void *data, size_t) {
const unsigned char *pixels = static_cast<const unsigned char *>(data);
delete []pixels;
}
static CGImageRef ImageCreateFromRGBA(int width, int height, const unsigned char *pixelsImage, bool invert) {
CGImageRef image = 0;
// Create an RGB color space.
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace) {
const int bitmapBytesPerRow = width * 4;
const int bitmapByteCount = bitmapBytesPerRow * height;
// Create a data provider.
CGDataProviderRef dataProvider = 0;
if (invert) {
unsigned char *pixelsUpsideDown = new unsigned char[bitmapByteCount];
for (int y=0; y<height; y++) {
int yInverse = height - y - 1;
memcpy(pixelsUpsideDown + y * bitmapBytesPerRow,
pixelsImage + yInverse * bitmapBytesPerRow,
bitmapBytesPerRow);
}
dataProvider = CGDataProviderCreateWithData(
NULL, pixelsUpsideDown, bitmapByteCount, ProviderReleaseData);
} else {
dataProvider = CGDataProviderCreateWithData(
NULL, pixelsImage, bitmapByteCount, NULL);
}
if (dataProvider) {
// Create the CGImage.
image = CGImageCreate(width,
height,
8,
8 * 4,
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaLast,
dataProvider,
NULL,
0,
kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);
}
// The image retains the color space, so we can release it.
CGColorSpaceRelease(colorSpace);
}
return image;
}
void SurfaceImpl::DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) {
CGImageRef image = ImageCreateFromRGBA(width, height, pixelsImage, true);
if (image) {
CGRect drawRect = CGRectMake(rc.left, rc.top, rc.Width(), rc.Height());
CGContextDrawImage(gc, drawRect, image);
CGImageRelease(image);
}
}
void SurfaceImpl::Ellipse(PRectangle rc, FillStroke fillStroke) {
const CGRect ellipseRect = CGRectFromPRectangleInset(rc, fillStroke.stroke.width / 2.0f);
SetFillStroke(fillStroke);
CGContextBeginPath(gc);
CGContextAddEllipseInRect(gc, ellipseRect);
CGContextDrawPath(gc, kCGPathFillStroke);
// Restore as not all paths set
CGContextSetLineWidth(gc, 1.0f);
}
void SurfaceImpl::Stadium(PRectangle rc, FillStroke fillStroke, Ends ends) {
const CGFloat midLine = rc.Centre().y;
const CGFloat piOn2 = acos(0.0);
const XYPOSITION halfStroke = fillStroke.stroke.width / 2.0f;
const float radius = rc.Height() / 2.0f - halfStroke;
PRectangle rcInner = rc;
rcInner.left += radius;
rcInner.right -= radius;
SetFillStroke(fillStroke);
CGContextBeginPath(gc);
const Ends leftSide = static_cast<Ends>(static_cast<int>(ends) & 0xf);
const Ends rightSide = static_cast<Ends>(static_cast<int>(ends) & 0xf0);
switch (leftSide) {
case Ends::leftFlat:
CGContextMoveToPoint(gc, rc.left + halfStroke, rc.top + halfStroke);
CGContextAddLineToPoint(gc, rc.left + halfStroke, rc.bottom - halfStroke);
break;
case Ends::leftAngle:
CGContextMoveToPoint(gc, rcInner.left + halfStroke, rc.top + halfStroke);
CGContextAddLineToPoint(gc, rc.left + halfStroke, rc.Centre().y);
CGContextAddLineToPoint(gc, rcInner.left + halfStroke, rc.bottom - halfStroke);
break;
case Ends::semiCircles:
default:
CGContextMoveToPoint(gc, rcInner.left + halfStroke, rc.top + halfStroke);
CGContextAddArc(gc, rcInner.left + halfStroke, midLine, radius, -piOn2, piOn2, 1);
break;
}
switch (rightSide) {
case Ends::rightFlat:
CGContextAddLineToPoint(gc, rc.right - halfStroke, rc.bottom - halfStroke);
CGContextAddLineToPoint(gc, rc.right - halfStroke, rc.top + halfStroke);
break;
case Ends::rightAngle:
CGContextAddLineToPoint(gc, rcInner.right - halfStroke, rc.bottom - halfStroke);
CGContextAddLineToPoint(gc, rc.right - halfStroke, rc.Centre().y);
CGContextAddLineToPoint(gc, rcInner.right - halfStroke, rc.top + halfStroke);
break;
case Ends::semiCircles:
default:
CGContextAddLineToPoint(gc, rcInner.right - halfStroke, rc.bottom - halfStroke);
CGContextAddArc(gc, rcInner.right - halfStroke, midLine, radius, piOn2, -piOn2, 1);
break;
}
// Close the path to enclose it for stroking and for filling, then draw it
CGContextClosePath(gc);
CGContextDrawPath(gc, kCGPathFillStroke);
CGContextSetLineWidth(gc, 1.0f);
}
void SurfaceImpl::CopyImageRectangle(SurfaceImpl *source, PRectangle srcRect, PRectangle dstRect) {
CGImageRef image = source->CreateImage();
CGRect src = PRectangleToCGRect(srcRect);
CGRect dst = PRectangleToCGRect(dstRect);
/* source from QuickDrawToQuartz2D.pdf on developer.apple.com */
const float w = static_cast<float>(CGImageGetWidth(image));
const float h = static_cast<float>(CGImageGetHeight(image));
CGRect drawRect = CGRectMake(0, 0, w, h);
if (!CGRectEqualToRect(src, dst)) {
CGFloat sx = CGRectGetWidth(dst) / CGRectGetWidth(src);
CGFloat sy = CGRectGetHeight(dst) / CGRectGetHeight(src);
CGFloat dx = CGRectGetMinX(dst) - (CGRectGetMinX(src) * sx);
CGFloat dy = CGRectGetMinY(dst) - (CGRectGetMinY(src) * sy);
drawRect = CGRectMake(dx, dy, w*sx, h*sy);
}
CGContextSaveGState(gc);
CGContextClipToRect(gc, dst);
CGContextDrawImage(gc, drawRect, image);
CGContextRestoreGState(gc);
CGImageRelease(image);
}
void SurfaceImpl::Copy(PRectangle rc, Scintilla::Internal::Point from, Surface &surfaceSource) {
// Maybe we have to make the Surface two contexts:
// a bitmap context which we do all the drawing on, and then a "real" context
// which we copy the output to when we call "Synchronize". Ugh! Gross and slow!
// For now, assume that copy can only be called on PixMap surfaces
SurfaceImpl &source = static_cast<SurfaceImpl &>(surfaceSource);
// Get the CGImageRef
CGImageRef image = source.CreateImage();
// If we could not get an image reference, fill the rectangle black
if (image == NULL) {
FillRectangle(rc, ColourRGBA::FromRGB(0));
return;
}
// Now draw the image on the surface
// Some fancy clipping work is required here: draw only inside of rc
CGContextSaveGState(gc);
CGContextClipToRect(gc, PRectangleToCGRect(rc));
//Platform::DebugPrintf(stderr, "Copy: CGContextDrawImage: (%d, %d) - (%d X %d)\n", rc.left - from.x, rc.top - from.y, source.bitmapWidth, source.bitmapHeight );
CGContextDrawImage(gc, CGRectMake(rc.left - from.x, rc.top - from.y, source.bitmapWidth, source.bitmapHeight), image);
// Undo the clipping fun
CGContextRestoreGState(gc);
// Done with the image
CGImageRelease(image);
image = NULL;
}
//--------------------------------------------------------------------------------------------------
// Bidirectional text support for Arabic and Hebrew.
std::unique_ptr<IScreenLineLayout> SurfaceImpl::Layout(const IScreenLine *screenLine) {
return std::make_unique<ScreenLineLayout>(screenLine);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::DrawTextNoClip(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text,
ColourRGBA fore, ColourRGBA back) {
FillRectangleAligned(rc, back);
DrawTextTransparent(rc, font_, ybase, text, fore);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::DrawTextClipped(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text,
ColourRGBA fore, ColourRGBA back) {
CGContextSaveGState(gc);
CGContextClipToRect(gc, PRectangleToCGRect(rc));
DrawTextNoClip(rc, font_, ybase, text, fore, back);
CGContextRestoreGState(gc);
}
//--------------------------------------------------------------------------------------------------
CFStringEncoding EncodingFromCharacterSet(bool unicode, CharacterSet characterSet) {
if (unicode)
return kCFStringEncodingUTF8;
// Unsupported -> Latin1 as reasonably safe
enum { notSupported = kCFStringEncodingISOLatin1};
switch (characterSet) {
case CharacterSet::Ansi:
return kCFStringEncodingISOLatin1;
case CharacterSet::Default:
return kCFStringEncodingISOLatin1;
case CharacterSet::Baltic:
return kCFStringEncodingWindowsBalticRim;
case CharacterSet::ChineseBig5:
return kCFStringEncodingBig5;
case CharacterSet::EastEurope:
return kCFStringEncodingWindowsLatin2;
case CharacterSet::GB2312:
return kCFStringEncodingGB_18030_2000;
case CharacterSet::Greek:
return kCFStringEncodingWindowsGreek;
case CharacterSet::Hangul:
return kCFStringEncodingEUC_KR;
case CharacterSet::Mac:
return kCFStringEncodingMacRoman;
case CharacterSet::Oem:
return kCFStringEncodingISOLatin1;
case CharacterSet::Russian:
return kCFStringEncodingKOI8_R;
case CharacterSet::Cyrillic:
return kCFStringEncodingWindowsCyrillic;
case CharacterSet::ShiftJis:
return kCFStringEncodingShiftJIS;
case CharacterSet::Symbol:
return kCFStringEncodingMacSymbol;
case CharacterSet::Turkish:
return kCFStringEncodingWindowsLatin5;
case CharacterSet::Johab:
return kCFStringEncodingWindowsKoreanJohab;
case CharacterSet::Hebrew:
return kCFStringEncodingWindowsHebrew;
case CharacterSet::Arabic:
return kCFStringEncodingWindowsArabic;
case CharacterSet::Vietnamese:
return kCFStringEncodingWindowsVietnamese;
case CharacterSet::Thai:
return kCFStringEncodingISOLatinThai;
case CharacterSet::Iso8859_15:
return kCFStringEncodingISOLatin1;
default:
return notSupported;
}
}
void SurfaceImpl::DrawTextTransparent(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text,
ColourRGBA fore) {
QuartzTextStyle *style = TextStyleFromFont(font_);
if (!style) {
return;
}
CFStringEncoding encoding = EncodingFromCharacterSet(UnicodeMode(), style->getCharacterSet());
CGColorRef color = CGColorCreateGenericRGB(fore.GetRedComponent(),
fore.GetGreenComponent(),
fore.GetBlueComponent(),
fore.GetAlphaComponent());
style->setCTStyleColour(color);
CGColorRelease(color);
QuartzTextLayout layoutDraw(text, encoding, style);
layoutDraw.draw(gc, rc.left, ybase);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::MeasureWidths(const Font *font_, std::string_view text, XYPOSITION *positions) {
const QuartzTextStyle *style = TextStyleFromFont(font_);
if (!style) {
return;
}
CFStringEncoding encoding = EncodingFromCharacterSet(UnicodeMode(), style->getCharacterSet());
QuartzTextLayout layoutMeasure(text, encoding, style);
const CFStringEncoding encodingUsed = layoutMeasure.getEncoding();
CTLineRef mLine = layoutMeasure.getCTLine();
assert(mLine);
if (encodingUsed != encoding) {
// Switched to MacRoman to make work so treat as single byte encoding.
for (int i=0; i<text.length(); i++) {
CGFloat xPosition = CTLineGetOffsetForStringIndex(mLine, i+1, nullptr);
positions[i] = xPosition;
}
return;
}
if (UnicodeMode()) {
// Map the widths given for UTF-16 characters back onto the UTF-8 input string
CFIndex fit = layoutMeasure.getStringLength();
int ui=0;
int i=0;
std::vector<CGFloat> linePositions(fit);
GetPositions(mLine, linePositions);
while (ui<fit) {
const unsigned char uch = text[i];
const unsigned int byteCount = UTF8BytesOfLead[uch];
const int codeUnits = UTF16LengthFromUTF8ByteCount(byteCount);
const CGFloat xPosition = linePositions[ui];
for (unsigned int bytePos=0; (bytePos<byteCount) && (i<text.length()); bytePos++) {
positions[i++] = xPosition;
}
ui += codeUnits;
}
XYPOSITION lastPos = 0.0f;
if (i > 0)
lastPos = positions[i-1];
while (i<text.length()) {
positions[i++] = lastPos;
}
} else if (mode.codePage) {
int ui = 0;
for (int i=0; i<text.length();) {
size_t lenChar = DBCSIsLeadByte(mode.codePage, text[i]) ? 2 : 1;
CGFloat xPosition = CTLineGetOffsetForStringIndex(mLine, ui+1, NULL);
for (unsigned int bytePos=0; (bytePos<lenChar) && (i<text.length()); bytePos++) {
positions[i++] = xPosition;
}
ui++;
}
} else { // Single byte encoding
for (int i=0; i<text.length(); i++) {
CGFloat xPosition = CTLineGetOffsetForStringIndex(mLine, i+1, NULL);
positions[i] = xPosition;
}
}
}
XYPOSITION SurfaceImpl::WidthText(const Font *font_, std::string_view text) {
const QuartzTextStyle *style = TextStyleFromFont(font_);
if (!style) {
return 1;
}
CFStringEncoding encoding = EncodingFromCharacterSet(UnicodeMode(), style->getCharacterSet());
QuartzTextLayout layoutMeasure(text, encoding, style);
return static_cast<XYPOSITION>(layoutMeasure.MeasureStringWidth());
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::DrawTextNoClipUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text,
ColourRGBA fore, ColourRGBA back) {
FillRectangleAligned(rc, back);
DrawTextTransparentUTF8(rc, font_, ybase, text, fore);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::DrawTextClippedUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text,
ColourRGBA fore, ColourRGBA back) {
CGContextSaveGState(gc);
CGContextClipToRect(gc, PRectangleToCGRect(rc));
DrawTextNoClipUTF8(rc, font_, ybase, text, fore, back);
CGContextRestoreGState(gc);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::DrawTextTransparentUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text,
ColourRGBA fore) {
QuartzTextStyle *style = TextStyleFromFont(font_);
if (!style) {
return;
}
const CFStringEncoding encoding = kCFStringEncodingUTF8;
CGColorRef color = CGColorCreateGenericRGB(fore.GetRedComponent(),
fore.GetGreenComponent(),
fore.GetBlueComponent(),
fore.GetAlphaComponent());
style->setCTStyleColour(color);
CGColorRelease(color);
QuartzTextLayout layoutDraw(text, encoding, style);
layoutDraw.draw(gc, rc.left, ybase);
}
//--------------------------------------------------------------------------------------------------
void SurfaceImpl::MeasureWidthsUTF8(const Font *font_, std::string_view text, XYPOSITION *positions) {
const QuartzTextStyle *style = TextStyleFromFont(font_);
if (!style) {
return;
}
constexpr CFStringEncoding encoding = kCFStringEncodingUTF8;
QuartzTextLayout layoutMeasure(text, encoding, style);
const CFStringEncoding encodingUsed = layoutMeasure.getEncoding();
CTLineRef mLine = layoutMeasure.getCTLine();
assert(mLine);
if (encodingUsed != encoding) {
// Switched to MacRoman to make work so treat as single byte encoding.
for (int i=0; i<text.length(); i++) {
CGFloat xPosition = CTLineGetOffsetForStringIndex(mLine, i+1, nullptr);
positions[i] = xPosition;
}
return;
}
// Map the widths given for UTF-16 characters back onto the UTF-8 input string
CFIndex fit = layoutMeasure.getStringLength();
int ui=0;
int i=0;
std::vector<CGFloat> linePositions(fit);
GetPositions(mLine, linePositions);
while (ui<fit) {
const unsigned char uch = text[i];
const unsigned int byteCount = UTF8BytesOfLead[uch];
const int codeUnits = UTF16LengthFromUTF8ByteCount(byteCount);
const CGFloat xPosition = linePositions[ui];
for (unsigned int bytePos=0; (bytePos<byteCount) && (i<text.length()); bytePos++) {
positions[i++] = xPosition;
}
ui += codeUnits;
}
XYPOSITION lastPos = 0.0f;
if (i > 0)
lastPos = positions[i-1];
while (i<text.length()) {
positions[i++] = lastPos;
}
}
XYPOSITION SurfaceImpl::WidthTextUTF8(const Font *font_, std::string_view text) {
const QuartzTextStyle *style = TextStyleFromFont(font_);
if (!style) {
return 1;
}
QuartzTextLayout layoutMeasure(text, kCFStringEncodingUTF8, style);
return static_cast<XYPOSITION>(layoutMeasure.MeasureStringWidth());
}
//--------------------------------------------------------------------------------------------------
// This string contains a good range of characters to test for size.
const char sizeString[] = "`~!@#$%^&*()-_=+\\|[]{};:\"\'<,>.?/1234567890"
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
XYPOSITION SurfaceImpl::Ascent(const Font *font_) {
const QuartzTextStyle *style = TextStyleFromFont(font_);
if (!style) {
return 1;
}
float ascent = style->getAscent();
return ascent + 0.5f;
}
XYPOSITION SurfaceImpl::Descent(const Font *font_) {
const QuartzTextStyle *style = TextStyleFromFont(font_);
if (!style) {
return 1;
}
float descent = style->getDescent();
return descent + 0.5f;
}
XYPOSITION SurfaceImpl::InternalLeading(const Font *) {
return 0;
}
XYPOSITION SurfaceImpl::Height(const Font *font_) {
return Ascent(font_) + Descent(font_);
}
XYPOSITION SurfaceImpl::AverageCharWidth(const Font *font_) {
XYPOSITION width = WidthText(font_, sizeString);
return std::round(width / strlen(sizeString));
}
void SurfaceImpl::SetClip(PRectangle rc) {
CGContextSaveGState(gc);
CGContextClipToRect(gc, PRectangleToCGRect(rc));
}
void SurfaceImpl::PopClip() {
CGContextRestoreGState(gc);
}
void SurfaceImpl::FlushCachedState() {
CGContextSynchronize(gc);
}
void SurfaceImpl::FlushDrawing() {
}
std::unique_ptr<Surface> Surface::Allocate(Technology) {
return std::make_unique<SurfaceImpl>();
}
//----------------- Window -------------------------------------------------------------------------
// Cocoa uses different types for windows and views, so a Window may
// be either an NSWindow or NSView and the code will check the type
// before performing an action.
Window::~Window() noexcept {
}
// Window::Destroy needs to see definition of ListBoxImpl so is located after ListBoxImpl
//--------------------------------------------------------------------------------------------------
static CGFloat ScreenMax() {
return NSMaxY([NSScreen mainScreen].frame);
}
//--------------------------------------------------------------------------------------------------
PRectangle Window::GetPosition() const {
if (wid) {
NSRect rect;
id idWin = (__bridge id)(wid);
NSWindow *win;
if ([idWin isKindOfClass: [NSView class]]) {
// NSView
NSView *view = idWin;
win = view.window;
rect = [view convertRect: view.bounds toView: nil];
rect = [win convertRectToScreen: rect];
} else {
// NSWindow
win = idWin;
rect = win.frame;
}
CGFloat screenHeight = ScreenMax();
// Invert screen positions to match Scintilla
return PRectangle(
NSMinX(rect), screenHeight - NSMaxY(rect),
NSMaxX(rect), screenHeight - NSMinY(rect));
} else {
return PRectangle(0, 0, 1, 1);
}
}
//--------------------------------------------------------------------------------------------------
void Window::SetPosition(PRectangle rc) {
if (wid) {
id idWin = (__bridge id)(wid);
if ([idWin isKindOfClass: [NSView class]]) {
// NSView
// Moves this view inside the parent view
NSRect nsrc = NSMakeRect(rc.left, rc.bottom, rc.Width(), rc.Height());
NSView *view = idWin;
nsrc = [view.window convertRectFromScreen: nsrc];
view.frame = nsrc;
} else {
// NSWindow
PLATFORM_ASSERT([idWin isKindOfClass: [NSWindow class]]);
NSWindow *win = idWin;
CGFloat screenHeight = ScreenMax();
NSRect nsrc = NSMakeRect(rc.left, screenHeight - rc.bottom,
rc.Width(), rc.Height());
[win setFrame: nsrc display: YES];
}
}
}
//--------------------------------------------------------------------------------------------------
void Window::SetPositionRelative(PRectangle rc, const Window *window) {
PRectangle rcOther = window->GetPosition();
rc.left += rcOther.left;
rc.right += rcOther.left;
rc.top += rcOther.top;
rc.bottom += rcOther.top;
SetPosition(rc);
}
//--------------------------------------------------------------------------------------------------
PRectangle Window::GetClientPosition() const {
// This means, in macOS terms, get the "frame bounds". Call GetPosition, just like on Win32.
return GetPosition();
}
//--------------------------------------------------------------------------------------------------
void Window::Show(bool show) {
if (wid) {
id idWin = (__bridge id)(wid);
if ([idWin isKindOfClass: [NSWindow class]]) {
NSWindow *win = idWin;
if (show) {
[win orderFront: nil];
} else {
[win orderOut: nil];
}
}
}
}
//--------------------------------------------------------------------------------------------------
/**
* Invalidates the entire window or view so it is completely redrawn.
*/
void Window::InvalidateAll() {
if (wid) {
id idWin = (__bridge id)(wid);
NSView *container;
if ([idWin isKindOfClass: [NSView class]]) {
container = idWin;
} else {
// NSWindow
NSWindow *win = idWin;
container = win.contentView;
container.needsDisplay = YES;
}
container.needsDisplay = YES;
}
}
//--------------------------------------------------------------------------------------------------
/**
* Invalidates part of the window or view so only this part redrawn.
*/
void Window::InvalidateRectangle(PRectangle rc) {
if (wid) {
id idWin = (__bridge id)(wid);
NSView *container;
if ([idWin isKindOfClass: [NSView class]]) {
container = idWin;
} else {
// NSWindow
NSWindow *win = idWin;
container = win.contentView;
}
[container setNeedsDisplayInRect: PRectangleToNSRect(rc)];
}
}
//--------------------------------------------------------------------------------------------------
/**
* Converts the Scintilla cursor enum into an NSCursor and stores it in the associated NSView,
* which then will take care to set up a new mouse tracking rectangle.
*/
void Window::SetCursor(Cursor curs) {
if (wid) {
id idWin = (__bridge id)(wid);
if ([idWin isKindOfClass: [SCIContentView class]]) {
SCIContentView *container = idWin;
[container setCursor: static_cast<int>(curs)];
}
}
}
//--------------------------------------------------------------------------------------------------
PRectangle Window::GetMonitorRect(Point) {
if (wid) {
id idWin = (__bridge id)(wid);
if ([idWin isKindOfClass: [NSView class]]) {
NSView *view = idWin;
idWin = view.window;
}
if ([idWin isKindOfClass: [NSWindow class]]) {
PRectangle rcPosition = GetPosition();
NSWindow *win = idWin;
NSScreen *screen = win.screen;
NSRect rect = screen.visibleFrame;
CGFloat screenHeight = rect.origin.y + rect.size.height;
// Invert screen positions to match Scintilla
PRectangle rcWork(
NSMinX(rect), screenHeight - NSMaxY(rect),
NSMaxX(rect), screenHeight - NSMinY(rect));
PRectangle rcMonitor(rcWork.left - rcPosition.left,
rcWork.top - rcPosition.top,
rcWork.right - rcPosition.left,
rcWork.bottom - rcPosition.top);
return rcMonitor;
}
}
return PRectangle();
}
//----------------- ImageFromXPM -------------------------------------------------------------------
// Convert an XPM image into an NSImage for use with Cocoa
static NSImage *ImageFromXPM(XPM *pxpm) {
NSImage *img = nil;
if (pxpm) {
const int width = pxpm->GetWidth();
const int height = pxpm->GetHeight();
PRectangle rcxpm(0, 0, width, height);
std::unique_ptr<Surface> surfaceBase(Surface::Allocate(Technology::Default));
std::unique_ptr<Surface> surfaceXPM = surfaceBase->AllocatePixMap(width, height);
SurfaceImpl *surfaceIXPM = static_cast<SurfaceImpl *>(surfaceXPM.get());
CGContextClearRect(surfaceIXPM->GetContext(), CGRectMake(0, 0, width, height));
pxpm->Draw(surfaceXPM.get(), rcxpm);
CGImageRef imageRef = surfaceIXPM->CreateImage();
img = [[NSImage alloc] initWithCGImage: imageRef size: NSZeroSize];
CGImageRelease(imageRef);
}
return img;
}
//----------------- ListBox and related classes ----------------------------------------------------
//----------------- IListBox -----------------------------------------------------------------------
namespace {
// Unnamed namespace hides local IListBox interface.
// IListBox is used to cross languages to send events from Objective C++
// AutoCompletionDelegate and AutoCompletionDataSource to C++ ListBoxImpl.
class IListBox {
public:
virtual int Rows() = 0;
virtual NSImage *ImageForRow(NSInteger row) = 0;
virtual NSString *TextForRow(NSInteger row) = 0;
virtual void DoubleClick() = 0;
virtual void SelectionChange() = 0;
};
}
//----------------- AutoCompletionDelegate ---------------------------------------------------------
// AutoCompletionDelegate is an Objective C++ class so it can implement
// NSTableViewDelegate and receive tableViewSelectionDidChange events.
@interface AutoCompletionDelegate : NSObject <NSTableViewDelegate> {
IListBox *box;
}
@property IListBox *box;
@end
@implementation AutoCompletionDelegate
@synthesize box;
- (void) tableViewSelectionDidChange: (NSNotification *) notification {
#pragma unused(notification)
if (box) {
box->SelectionChange();
}
}
@end
//----------------- AutoCompletionDataSource -------------------------------------------------------
// AutoCompletionDataSource provides data to display in the list box.
// It is also the target of the NSTableView so it receives double clicks.
@interface AutoCompletionDataSource : NSObject <NSTableViewDataSource> {
IListBox *box;
}
@property IListBox *box;
@end
@implementation AutoCompletionDataSource
@synthesize box;
- (void) doubleClick: (id) sender {
#pragma unused(sender)
if (box) {
box->DoubleClick();
}
}
- (id) tableView: (NSTableView *) aTableView objectValueForTableColumn: (NSTableColumn *) aTableColumn row: (NSInteger) rowIndex {
#pragma unused(aTableView)
if (!box)
return nil;
if ([(NSString *)aTableColumn.identifier isEqualToString: @"icon"]) {
return box->ImageForRow(rowIndex);
} else {
return box->TextForRow(rowIndex);
}
}
- (void) tableView: (NSTableView *) aTableView setObjectValue: anObject forTableColumn: (NSTableColumn *) aTableColumn row: (NSInteger) rowIndex {
#pragma unused(aTableView)
#pragma unused(anObject)
#pragma unused(aTableColumn)
#pragma unused(rowIndex)
}
- (NSInteger) numberOfRowsInTableView: (NSTableView *) aTableView {
#pragma unused(aTableView)
if (!box)
return 0;
return box->Rows();
}
@end
//----------------- ListBoxImpl --------------------------------------------------------------------
namespace { // unnamed namespace hides ListBoxImpl and associated classes
struct RowData {
int type;
std::string text;
RowData(int type_, const char *text_) :
type(type_), text(text_) {
}
};
class LinesData {
std::vector<RowData> lines;
public:
LinesData() {
}
~LinesData() {
}
int Length() const {
return static_cast<int>(lines.size());
}
void Clear() {
lines.clear();
}
void Add(int /* index */, int type, char *str) {
lines.push_back(RowData(type, str));
}
int GetType(size_t index) const {
if (index < lines.size()) {
return lines[index].type;
} else {
return 0;
}
}
const char *GetString(size_t index) const {
if (index < lines.size()) {
return lines[index].text.c_str();
} else {
return 0;
}
}
};
class ListBoxImpl : public ListBox, IListBox {
private:
NSMutableDictionary *images;
int lineHeight;
bool unicodeMode;
int desiredVisibleRows;
XYPOSITION maxItemWidth;
unsigned int aveCharWidth;
XYPOSITION maxIconWidth;
std::unique_ptr<Font> font;
int maxWidth;
NSTableView *table;
NSScrollView *scroller;
NSTableColumn *colIcon;
NSTableColumn *colText;
AutoCompletionDataSource *ds;
AutoCompletionDelegate *acd;
LinesData ld;
IListBoxDelegate *delegate;
public:
ListBoxImpl() :
images(nil),
lineHeight(10),
unicodeMode(false),
desiredVisibleRows(5),
maxItemWidth(0),
aveCharWidth(8),
maxIconWidth(0),
maxWidth(2000),
table(nil),
scroller(nil),
colIcon(nil),
colText(nil),
ds(nil),
acd(nil),
delegate(nullptr) {
images = [[NSMutableDictionary alloc] init];
}
~ListBoxImpl() override {
}
// ListBox methods
void SetFont(const Font *font_) override;
void Create(Window &parent, int ctrlID, Scintilla::Internal::Point pt, int lineHeight_, bool unicodeMode_, Technology technology_) override;
void SetAverageCharWidth(int width) override;
void SetVisibleRows(int rows) override;
int GetVisibleRows() const override;
PRectangle GetDesiredRect() override;
int CaretFromEdge() override;
void Clear() noexcept override;
void Append(char *s, int type = -1) override;
int Length() override;
void Select(int n) override;
int GetSelection() override;
int Find(const char *prefix) override;
std::string GetValue(int n) override;
void RegisterImage(int type, const char *xpm_data) override;
void RegisterRGBAImage(int type, int width, int height, const unsigned char *pixelsImage) override;
void ClearRegisteredImages() override;
void SetDelegate(IListBoxDelegate *lbDelegate) override {
delegate = lbDelegate;
}
void SetList(const char *list, char separator, char typesep) override;
void SetOptions(ListOptions options_) override;
// To clean up when closed
void ReleaseViews();
// For access from AutoCompletionDataSource implement IListBox
int Rows() override;
NSImage *ImageForRow(NSInteger row) override;
NSString *TextForRow(NSInteger row) override;
void DoubleClick() override;
void SelectionChange() override;
};
void ListBoxImpl::Create(Window & /*parent*/, int /*ctrlID*/, Scintilla::Internal::Point pt,
int lineHeight_, bool unicodeMode_, Technology) {
lineHeight = lineHeight_;
unicodeMode = unicodeMode_;
maxWidth = 2000;
NSRect lbRect = NSMakeRect(pt.x, pt.y, 120, lineHeight * desiredVisibleRows);
NSWindow *winLB = [[NSWindow alloc] initWithContentRect: lbRect
styleMask: NSWindowStyleMaskBorderless
backing: NSBackingStoreBuffered
defer: NO];
[winLB setLevel: NSModalPanelWindowLevel+1];
[winLB setHasShadow: YES];
NSRect scRect = NSMakeRect(0, 0, lbRect.size.width, lbRect.size.height);
scroller = [[NSScrollView alloc] initWithFrame: scRect];
[scroller setHasVerticalScroller: YES];
table = [[NSTableView alloc] initWithFrame: scRect];
[table setHeaderView: nil];
scroller.documentView = table;
colIcon = [[NSTableColumn alloc] initWithIdentifier: @"icon"];
colIcon.width = 20;
[colIcon setEditable: NO];
[colIcon setHidden: YES];
NSImageCell *imCell = [[NSImageCell alloc] init];
colIcon.dataCell = imCell;
[table addTableColumn: colIcon];
colText = [[NSTableColumn alloc] initWithIdentifier: @"name"];
colText.resizingMask = NSTableColumnAutoresizingMask;
[colText setEditable: NO];
[table addTableColumn: colText];
ds = [[AutoCompletionDataSource alloc] init];
ds.box = this;
table.dataSource = ds; // Weak reference
acd = [[AutoCompletionDelegate alloc] init];
[acd setBox: this];
table.delegate = acd;
scroller.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[winLB.contentView addSubview: scroller];
table.target = ds;
table.doubleAction = @selector(doubleClick:);
table.selectionHighlightStyle = NSTableViewSelectionHighlightStyleSourceList;
if (@available(macOS 11.0, *)) {
[table setStyle: NSTableViewStylePlain];
}
wid = (__bridge_retained WindowID)winLB;
}
void ListBoxImpl::SetFont(const Font *font_) {
// NSCell setFont takes an NSFont* rather than a CTFontRef but they
// are the same thing toll-free bridged.
QuartzTextStyle *style = TextStyleFromFont(font_);
font = std::make_unique<FontQuartz>(style);
NSFont *pfont = (__bridge NSFont *)style->getFontRef();
[colText.dataCell setFont: pfont];
CGFloat itemHeight = std::ceil(pfont.boundingRectForFont.size.height);
table.rowHeight = itemHeight;
}
void ListBoxImpl::SetAverageCharWidth(int width) {
aveCharWidth = width;
}
void ListBoxImpl::SetVisibleRows(int rows) {
desiredVisibleRows = rows;
}
int ListBoxImpl::GetVisibleRows() const {
return desiredVisibleRows;
}
PRectangle ListBoxImpl::GetDesiredRect() {
PRectangle rcDesired;
rcDesired = GetPosition();
CGFloat itemHeight;
if (@available(macOS 11.0, *)) {
itemHeight = table.rowHeight;
} else {
// There appears to be an extra pixel above and below the row contents
itemHeight = table.rowHeight + 2;
}
int rows = Length();
if ((rows == 0) || (rows > desiredVisibleRows))
rows = desiredVisibleRows;
rcDesired.bottom = rcDesired.top + static_cast<XYPOSITION>(itemHeight * rows);
rcDesired.right = rcDesired.left + maxItemWidth + aveCharWidth;
rcDesired.right += 4; // Ensures no truncation of text
if (Length() > rows) {
[scroller setHasVerticalScroller: YES];
rcDesired.right += [NSScroller scrollerWidthForControlSize: NSControlSizeRegular
scrollerStyle: NSScrollerStyleLegacy];
} else {
[scroller setHasVerticalScroller: NO];
}
rcDesired.right += maxIconWidth;
rcDesired.right += 6; // For icon space
return rcDesired;
}
int ListBoxImpl::CaretFromEdge() {
if (colIcon.hidden)
return 3;
else
return 6 + static_cast<int>(colIcon.width);
}
void ListBoxImpl::ReleaseViews() {
[table setDataSource: nil];
table = nil;
scroller = nil;
colIcon = nil;
colText = nil;
acd = nil;
ds = nil;
}
void ListBoxImpl::Clear() noexcept {
maxItemWidth = 0;
maxIconWidth = 0;
ld.Clear();
}
void ListBoxImpl::Append(char *s, int type) {
int count = Length();
ld.Add(count, type, s);
Scintilla::Internal::SurfaceImpl surface;
XYPOSITION width = surface.WidthText(font.get(), s);
if (width > maxItemWidth) {
maxItemWidth = width;
colText.width = maxItemWidth;
}
NSImage *img = images[@(type)];
if (img) {
XYPOSITION widthIcon = img.size.width;
if (widthIcon > maxIconWidth) {
[colIcon setHidden: NO];
maxIconWidth = widthIcon;
colIcon.width = maxIconWidth;
}
}
}
void ListBoxImpl::SetList(const char *list, char separator, char typesep) {
Clear();
size_t count = strlen(list) + 1;
std::vector<char> words(list, list+count);
char *startword = words.data();
char *numword = nullptr;
int i = 0;
for (; words[i]; i++) {
if (words[i] == separator) {
words[i] = '\0';
if (numword)
*numword = '\0';
Append(startword, numword?atoi(numword + 1):-1);
startword = words.data() + i + 1;
numword = nullptr;
} else if (words[i] == typesep) {
numword = words.data() + i;
}
}
if (startword) {
if (numword)
*numword = '\0';
Append(startword, numword?atoi(numword + 1):-1);
}
[table reloadData];
}
void ListBoxImpl::SetOptions(ListOptions) {
}
int ListBoxImpl::Length() {
return ld.Length();
}
void ListBoxImpl::Select(int n) {
[table selectRowIndexes: [NSIndexSet indexSetWithIndex: n] byExtendingSelection: NO];
[table scrollRowToVisible: n];
}
int ListBoxImpl::GetSelection() {
return static_cast<int>(table.selectedRow);
}
int ListBoxImpl::Find(const char *prefix) {
int count = Length();
for (int i = 0; i < count; i++) {
const char *s = ld.GetString(i);
if (s && (s[0] != '\0') && (0 == strncmp(prefix, s, strlen(prefix)))) {
return i;
}
}
return - 1;
}
std::string ListBoxImpl::GetValue(int n) {
const char *textString = ld.GetString(n);
if (textString) {
return textString;
}
return std::string();
}
void ListBoxImpl::RegisterImage(int type, const char *xpm_data) {
XPM xpm(xpm_data);
NSImage *img = ImageFromXPM(&xpm);
images[@(type)] = img;
}
void ListBoxImpl::RegisterRGBAImage(int type, int width, int height, const unsigned char *pixelsImage) {
CGImageRef imageRef = ImageCreateFromRGBA(width, height, pixelsImage, false);
NSImage *img = [[NSImage alloc] initWithCGImage: imageRef size: NSZeroSize];
CGImageRelease(imageRef);
images[@(type)] = img;
}
void ListBoxImpl::ClearRegisteredImages() {
[images removeAllObjects];
}
int ListBoxImpl::Rows() {
return ld.Length();
}
NSImage *ListBoxImpl::ImageForRow(NSInteger row) {
return images[@(ld.GetType(row))];
}
NSString *ListBoxImpl::TextForRow(NSInteger row) {
const char *textString = ld.GetString(row);
NSString *sTitle;
if (unicodeMode)
sTitle = @(textString);
else
sTitle = [NSString stringWithCString: textString encoding: NSWindowsCP1252StringEncoding];
return sTitle;
}
void ListBoxImpl::DoubleClick() {
if (delegate) {
ListBoxEvent event(ListBoxEvent::EventType::doubleClick);
delegate->ListNotify(&event);
}
}
void ListBoxImpl::SelectionChange() {
if (delegate) {
ListBoxEvent event(ListBoxEvent::EventType::selectionChange);
delegate->ListNotify(&event);
}
}
} // unnamed namespace
//----------------- ListBox ------------------------------------------------------------------------
// ListBox is implemented by the ListBoxImpl class.
ListBox::ListBox() noexcept {
}
ListBox::~ListBox() noexcept {
}
std::unique_ptr<ListBox> ListBox::Allocate() {
return std::make_unique<ListBoxImpl>();
}
//--------------------------------------------------------------------------------------------------
void Window::Destroy() noexcept {
ListBoxImpl *listbox = dynamic_cast<ListBoxImpl *>(this);
if (listbox) {
listbox->ReleaseViews();
}
if (wid) {
id idWin = (__bridge id)(wid);
if ([idWin isKindOfClass: [NSWindow class]]) {
[idWin close];
}
}
wid = nullptr;
}
//----------------- ScintillaContextMenu -----------------------------------------------------------
@implementation ScintillaContextMenu :
NSMenu
// This NSMenu subclass serves also as target for menu commands and forwards them as
// notification messages to the front end.
- (void) handleCommand: (NSMenuItem *) sender {
owner->HandleCommand(sender.tag);
}
//--------------------------------------------------------------------------------------------------
- (void) setOwner: (Scintilla::Internal::ScintillaCocoa *) newOwner {
owner = newOwner;
}
@end
//----------------- Menu ---------------------------------------------------------------------------
Menu::Menu() noexcept
: mid(0) {
}
//--------------------------------------------------------------------------------------------------
void Menu::CreatePopUp() {
Destroy();
mid = (__bridge_retained MenuID)[[ScintillaContextMenu alloc] initWithTitle: @""];
}
//--------------------------------------------------------------------------------------------------
void Menu::Destroy() noexcept {
CFBridgingRelease(mid);
mid = nullptr;
}
//--------------------------------------------------------------------------------------------------
void Menu::Show(Point, const Window &) {
// Cocoa menus are handled a bit differently. We only create the menu. The framework
// takes care to show it properly.
}
//----------------- Platform -----------------------------------------------------------------------
ColourRGBA Platform::Chrome() {
return ColourRGBA(0xE0, 0xE0, 0xE0);
}
//--------------------------------------------------------------------------------------------------
ColourRGBA Platform::ChromeHighlight() {
return ColourRGBA(0xFF, 0xFF, 0xFF);
}
//--------------------------------------------------------------------------------------------------
/**
* Returns the currently set system font for the user.
*/
const char *Platform::DefaultFont() {
return "Menlo-Regular";
}
//--------------------------------------------------------------------------------------------------
/**
* Returns the currently set system font size for the user.
*/
int Platform::DefaultFontSize() {
return 11;
}
//--------------------------------------------------------------------------------------------------
/**
* Returns the time span in which two consecutive mouse clicks must occur to be considered as
* double click.
*
* @return time span in milliseconds
*/
unsigned int Platform::DoubleClickTime() {
NSTimeInterval threshold = NSEvent.doubleClickInterval;
if (threshold == 0)
threshold = 0.5;
return static_cast<unsigned int>(threshold * 1000.0);
}
//--------------------------------------------------------------------------------------------------
//#define TRACE
#ifdef TRACE
void Platform::DebugDisplay(const char *s) noexcept {
fprintf(stderr, "%s", s);
}
//--------------------------------------------------------------------------------------------------
void Platform::DebugPrintf(const char *format, ...) noexcept {
const int BUF_SIZE = 2000;
char buffer[BUF_SIZE];
va_list pArguments;
va_start(pArguments, format);
vsnprintf(buffer, BUF_SIZE, format, pArguments);
va_end(pArguments);
Platform::DebugDisplay(buffer);
}
#else
void Platform::DebugDisplay(const char *) noexcept {}
void Platform::DebugPrintf(const char *, ...) noexcept {}
#endif
//--------------------------------------------------------------------------------------------------
static bool assertionPopUps = true;
bool Platform::ShowAssertionPopUps(bool assertionPopUps_) noexcept {
bool ret = assertionPopUps;
assertionPopUps = assertionPopUps_;
return ret;
}
//--------------------------------------------------------------------------------------------------
void Platform::Assert(const char *c, const char *file, int line) noexcept {
char buffer[2000];
snprintf(buffer, sizeof(buffer), "Assertion [%s] failed at %s %d\r\n", c, file, line);
Platform::DebugDisplay(buffer);
#ifdef DEBUG
// Jump into debugger in assert on Mac
pthread_kill(pthread_self(), SIGTRAP);
#endif
}
//--------------------------------------------------------------------------------------------------