Cocoa: CocoaGDIObjects TCocoaContext.TextOut text drawing optimization, patch by David Jenkins (try 2), issue #39641

This commit is contained in:
Maxim Ganetsky 2022-02-12 02:38:52 +03:00
parent a297b6b623
commit ffd4757217
2 changed files with 183 additions and 387 deletions

View File

@ -343,45 +343,6 @@ type
destructor Destroy; override;
end;
TGlyphArray = array of NSGlyph;
{ TCocoaTextLayout }
TCocoaTextLayout = class
strict private
FBackgroundColor: TColor;
FForegroundColor: TColor;
FLayout: NSLayoutManager;
FTextStorage: NSTextStorage;
FTextContainer: NSTextContainer;
FText: String;
FFont: TCocoaFont;
// surrogate pairs (for UTF16)
FSurr: array of NSRange;
FSurrCount: Integer;
procedure SetBackgoundColor(AValue: TColor);
procedure SetForegoundColor(AValue: TColor);
procedure SetFont(AFont: TCocoaFont);
procedure UpdateFont;
procedure UpdateColor;
function GetTextRange: NSRange;
procedure EvalSurrogate(s: NSString);
public
constructor Create;
destructor Destroy; override;
procedure SetFontToStr(dst: NSMutableAttributedString);
procedure SetText(UTF8Text: PChar; ByteSize: Integer);
function GetSize: TSize;
function GetGlyphs: TGlyphArray;
procedure Draw(ctx: NSGraphicsContext; X, Y: Integer; FillBackground: Boolean; DX: PInteger);
property Font: TCocoaFont read FFont write SetFont;
property BackgroundColor: TColor read FBackgroundColor write SetBackgoundColor;
property ForegroundColor: TColor read FForegroundColor write SetForegoundColor;
property Layout: NSLayoutManager read FLayout;
end;
{ TCocoaContext }
TCocoaBitmapContext = class;
@ -391,8 +352,10 @@ type
FBkColor: TColor;
FBkMode: Integer;
FROP2: Integer;
FText : TCocoaTextLayout;
FBrush : TCocoaBrush;
FBackgroundColor: TColor;
FForegroundColor: TColor;
FFont: TCocoaFont;
FPen : TCocoaPen;
FRegion : TCocoaRegion;
// In Cocoa there is no way to enlarge a clip region :(
@ -1280,297 +1243,6 @@ begin
NSCursor.arrowCursor.set_;
end;
{ TCocoaTextLayout }
procedure TCocoaTextLayout.UpdateFont;
begin
SetFontToStr(FTextStorage);
end;
procedure TCocoaTextLayout.SetFontToStr(dst: NSMutableAttributedString);
const
UnderlineStyle = NSUnderlineStyleSingle or NSUnderlinePatternSolid;
var
Range: NSRange;
begin
if Assigned(FFont) then
begin
Range.location := 0;
Range.length := dst.string_.length;
if (Range.length <= 0) or (FFont.Font = nil) then Exit;
// apply font itself
dst.addAttribute_value_range(NSFontAttributeName, FFont.Font, Range);
// aply font attributes which are not in NSFont
if cfs_Underline in FFont.Style then
dst.addAttribute_value_range(NSUnderlineStyleAttributeName, NSNumber.numberWithInteger(UnderlineStyle), Range)
else
dst.removeAttribute_range(NSUnderlineStyleAttributeName, Range);
if cfs_Strikeout in FFont.Style then
dst.addAttribute_value_range(NSStrikethroughStyleAttributeName, NSNumber.numberWithInteger(UnderlineStyle), Range)
else
dst.removeAttribute_range(NSStrikethroughStyleAttributeName, Range);
end;
end;
procedure TCocoaTextLayout.UpdateColor;
var
lForegroundColor: NSColor;
begin
lForegroundColor := SysColorToNSColor(SysColorToSysColorIndex(ForegroundColor));
if lForegroundColor = nil then
lForegroundColor := ColorToNSColor(ColorToRGB(ForegroundColor));
FTextStorage.addAttribute_value_range(NSForegroundColorAttributeName, lForegroundColor, GetTextRange);
FTextStorage.addAttribute_value_range(NSBackgroundColorAttributeName, ColorToNSColor(BackgroundColor), GetTextRange);
end;
function TCocoaTextLayout.GetTextRange: NSRange;
begin
Result.location := 0;
Result.length := FTextStorage.length;
end;
procedure TCocoaTextLayout.EvalSurrogate(s: NSString);
var
res : NSRange;
i : integer;
ln : integer;
scnt : integer;
ch : integer;
begin
FSurrCount := 0;
i := 0;
ln := s.length;
// must analyze the string to presence of surrogate pairs.
// this is required for the use
ch := 0;
while i < ln do
begin
res := s.rangeOfComposedCharacterSequenceAtIndex(i); //s.rangeOfComposedCharacterSequencesForRange(src);
inc(i, res.length);
if res.length>1 then
begin
if length(FSurr) = FSurrCount then
begin
if FSurrCount = 0 then SetLength(FSurr, 4)
else SetLength(FSurr, FSurrCount * 2)
end;
FSurr[FSurrCount] := res;
inc(fSurrCount);
end;
inc(ch);
end;
if ((FSurrCount = 0) and (length(FSurr)<>0)) or (length(FSurr) div 2>FSurrCount) then
SetLength(FSurr, FSurrCount);
end;
procedure TCocoaTextLayout.SetForegoundColor(AValue: TColor);
begin
if FForegroundColor <> AValue then
begin
FForegroundColor := AValue;
FTextStorage.beginEditing;
UpdateColor;
FTextStorage.endEditing;
end;
end;
procedure TCocoaTextLayout.SetBackgoundColor(AValue: TColor);
begin
if FBackgroundColor <> AValue then
begin
FBackgroundColor := AValue;
FTextStorage.beginEditing;
UpdateColor;
FTextStorage.endEditing;
end;
end;
constructor TCocoaTextLayout.Create;
var
LocalPool: NSAutoReleasePool;
begin
inherited Create;
LocalPool := NSAutoReleasePool.alloc.init;
FTextStorage := NSTextStorage.alloc.initWithString(NSSTR(''));
FLayout := NSLayoutManager.alloc.init;
FTextStorage.addLayoutManager(FLayout);
FTextContainer := NSTextContainer.alloc.init;
FTextContainer.setLineFragmentPadding(0);
FLayout.addTextContainer(FTextContainer);
LocalPool.release;
FFont := DefaultFont;
FFont.AddRef;
FText := '';
FBackgroundColor := clWhite;
FForegroundColor := clBlack;
end;
destructor TCocoaTextLayout.Destroy;
begin
FLayout.release;
FTextContainer.release;
FTextStorage.release;
if Assigned(FFont) then
FFont.Release;
inherited Destroy;
end;
procedure TCocoaTextLayout.SetFont(AFont: TCocoaFont);
begin
if TCocoaGDIObject.UpdateRefs(FFont, AFont) then
begin
FFont := AFont as TCocoaFont;
FTextStorage.beginEditing;
updateFont;
FTextStorage.endEditing;
end;
end;
procedure TCocoaTextLayout.SetText(UTF8Text: PChar; ByteSize: Integer);
var
NewText: String;
S: NSString;
begin
if ByteSize >= 0 then
System.SetString(NewText, UTF8Text, ByteSize)
else
NewText := StrPas(UTF8Text);
if FText <> NewText then
begin
FText := NewText;
S := NSStringUTF8(NewText);
try
FSurrCount:=-1; // invalidating surragete pair search
FTextStorage.beginEditing;
if S <> nil then
FTextStorage.replaceCharactersInRange_withString(GetTextRange, S);
updateFont;
updateColor;
FTextStorage.endEditing;
except
end;
S.release;
end;
end;
function TCocoaTextLayout.GetSize: TSize;
var
Range: NSRange;
bnds: NSRect;
begin
Range := FLayout.glyphRangeForTextContainer(FTextContainer);
//for text with soft-breaks (#13) the vertical bounds is too high!
//(feels like it tryes to span it from top to bottom)
//bnds := FLayout.boundingRectForGlyphRange_inTextContainer(Range, FTextContainer);
bnds := FLayout.usedRectForTextContainer(FTextContainer);
Result.cx := Round(bnds.size.width);
Result.cy := Round(bnds.size.height);
end;
function TCocoaTextLayout.GetGlyphs: TGlyphArray;
var
Range: NSRange;
begin
Range := FLayout.glyphRangeForTextContainer(FTextContainer);
// required length + 1 space
SetLength(Result, Range.length + 1);
FLayout.getGlyphs_range(@Result[0], Range);
SetLength(Result, Range.length);
end;
procedure TCocoaTextLayout.Draw(ctx: NSGraphicsContext; X, Y: Integer; FillBackground: Boolean; DX: PInteger);
var
Range: NSRange;
Pt: NSPoint;
Context: NSGraphicsContext;
Locations: array of NSPoint;
Indexes: array of NSUInteger;
I,j, Count, ii: NSUInteger;
si: Integer;
transform : NSAffineTransform;
begin
Range := FLayout.glyphRangeForTextContainer(FTextContainer);
if Range.length = 0 then
Exit; // cannot render anything. string is empty or invalid characters
if not ctx.isFlipped then
Context := NSGraphicsContext.graphicsContextWithGraphicsPort_flipped(ctx.graphicsPort, True)
else
Context := ctx;
NSGraphicsContext.classSaveGraphicsState;
NSGraphicsContext.setCurrentContext(Context);
ctx.setShouldAntialias(FFont.Antialiased);
if FFont.RotationDeg<>0 then
begin
transform := NSAffineTransform.transform;
transform.translateXBy_yBy(X, Y);
if ctx.isFlipped then
transform.rotateByDegrees( FFont.RotationDeg )
else
transform.rotateByDegrees( -FFont.RotationDeg );
transform.translateXBy_yBy(-X, -Y);
transform.concat;
end;
Pt.x := X;
Pt.y := Y;
if Assigned(DX) then
begin
// DX - is provided for UTF8 characters. UTF8 doesn't have surrogate pairs
// UTF16 does. UTF16 is the base for NSTextLayout and NSString.
// Thus for any character in DX, there might be mulitple "characeters"
// in NSTextLayout. See #35675.
if FSurrCount<0 then EvalSurrogate(FTextStorage.string_);
Count := Range.length;
SetLength(Locations, Count);
SetLength(Indexes, Count);
Locations[0] := FLayout.locationForGlyphAtIndex(0);
Indexes[0] := 0;
for I := 1 to Count - 1 do Indexes[I] := I;
// no surrogate pairs
I := 1;
j := 0;
if FSurrCount > 0 then
begin
si := 0;
for si:=0 to FSurrCount - 1 do
begin
for ii := i to FSurr[si].location do
begin
Locations[I] := Locations[I - 1];
Locations[I].x := Locations[I].x + DX[J];
inc(i);
inc(j);
end;
for ii := 2 to FSurr[si].length do
begin
Locations[I] := Locations[I - 1];
inc(I);
end;
end;
end;
// remaining DX offsets
for I := I to Count - 1 do
begin
Locations[I] := Locations[I - 1];
Locations[I].x := Locations[I].x + DX[J];
inc(j);
end;
FLayout.setLocations_startingGlyphIndexes_count_forGlyphRange(@Locations[0], @Indexes[0], Count, Range);
end;
if FillBackground then
FLayout.drawBackgroundForGlyphRange_atPoint(Range, Pt);
FLayout.drawGlyphsForGlyphRange_atPoint(Range, Pt);
NSGraphicsContext.classRestoreGraphicsState;
end;
{ TCocoaContext }
@ -1625,12 +1297,12 @@ end;
function TCocoaContext.GetTextColor: TColor;
begin
Result := FText.ForegroundColor;
Result := FForegroundColor;
end;
function TCocoaContext.GetFont: TCocoaFont;
begin
Result := FText.Font;
Result := FFont;
end;
procedure TCocoaContext.SetBkColor(AValue: TColor);
@ -1660,7 +1332,7 @@ end;
procedure TCocoaContext.SetFont(const AValue: TCocoaFont);
begin
FText.Font := AValue; // UpdateRefs done within property setter
FFont := AValue; // UpdateRefs done within property setter
end;
procedure TCocoaContext.SetPen(const AValue: TCocoaPen);
@ -1693,7 +1365,7 @@ end;
procedure TCocoaContext.SetTextColor(AValue: TColor);
begin
FText.ForegroundColor := AValue;
FForegroundColor := AValue;
end;
procedure TCocoaContext.UpdateContextOfs(const AWindowOfs, AViewOfs: TPoint);
@ -1802,7 +1474,6 @@ begin
FClipRegion.AddRef;
FSavedDCList := nil;
FText := TCocoaTextLayout.Create;
FClipped := False;
FFlipped := False;
end;
@ -1819,7 +1490,6 @@ begin
FClipRegion.Release;
FSavedDCList.Free;
FText.Free;
FBkBrush.Free;
@ -2172,10 +1842,32 @@ begin
end;
procedure TCocoaContext.TextOut(X, Y: Integer; Options: Longint; Rect: PRect; UTF8Chars: PChar; Count: Integer; CharsDelta: PInteger);
const
UnderlineStyle = NSUnderlineStyleSingle or NSUnderlinePatternSolid;
var
cg: CGContextRef;
BrushSolid, FillBg: Boolean;
AttribStr: CFAttributedStringRef;
Str: NSString;
CoreLine: CTLineRef;
Glyphcount, k, i, CurGlyphCount: integer;
Locations: array of NSPoint;
Runs: CFArrayRef;
CurRun: CTRunRef;
RunCount: Integer;
Glyphs: array of CGGlyph;
RunFont: CTFontRef;
transform: NSAffineTransform;
Dict: NSMutableDictionary;
YPrime, StrikeH, StrikeW: CGFloat;
lForegroundColor: NSColor;
begin
CGContextSaveGState(CGContext());
cg := CGContext();
if not Assigned(cg) then
Exit;
CGContextSaveGState(cg);
CGContextSetTextMatrix(cg, CGAffineTransformIdentity);
if Assigned(Rect) then
begin
@ -2192,9 +1884,9 @@ begin
if ((Options and ETO_CLIPPED) <> 0) and (Count > 0) then
begin
CGContextBeginPath(CGContext);
CGContextAddRect(CGContext, RectToCGrect(Rect^));
CGContextClip(CGContext);
CGContextBeginPath(cg);
CGContextAddRect(cg, RectToCGrect(Rect^));
CGContextClip(cg);
end;
end;
@ -2202,12 +1894,105 @@ begin
begin
FillBg := BkMode = OPAQUE;
if FillBg then
FText.BackgroundColor := BkBrush.ColorRef;
FText.SetText(UTF8Chars, Count);
FText.Draw(ctx, X, Y, FillBg, CharsDelta);
FBackgroundColor := BkBrush.ColorRef;
Str := NSStringUTF8(UTF8Chars, Count);
try
lForegroundColor := SysColorToNSColor(SysColorToSysColorIndex(FForegroundColor));
if lForegroundColor = nil then
lForegroundColor := ColorToNSColor(ColorToRGB(FForegroundColor));
Dict := NSMutableDictionary.dictionaryWithObjectsAndKeys(
Font.Font, kCTFontAttributeName,
lForegroundColor, NSForegroundColorAttributeName,
nil);
if FillBg then
Dict.setObject_forKey(ColorToNSColor(FBackgroundColor), NSBackgroundColorAttributeName);
if cfs_Underline in Font.Style then
Dict.setObject_forKey(NSNumber.numberWithInteger(UnderlineStyle), NSUnderlineStyleAttributeName);
AttribStr := CFAttributedStringCreate(kCFAllocatorDefault, CFStringRef(Str), CFDictionaryRef(Dict));
try
CoreLine := CTLineCreateWithAttributedString(CFAttributedStringRef(AttribStr));
try
ctx.setShouldAntialias(Font.Antialiased);
if FFont.RotationDeg <> 0 then
begin
transform := NSAffineTransform.transform;
transform.translateXBy_yBy(X, Y);
if ctx.isFlipped then
transform.rotateByDegrees( FFont.RotationDeg )
else
transform.rotateByDegrees( -FFont.RotationDeg );
transform.translateXBy_yBy(-X, -Y);
transform.concat;
end;
CGContextTranslateCTM(cg, 0, FSize.Height);
CGContextScaleCTM(cg, 1, -1);
YPrime := FSize.Height - y - Font.Font.ascender;
CGContextSetTextPosition(cg, x, YPrime);
if CharsDelta = nil then
begin
CTLineDraw(CoreLine, cg);
end
else // CharsDelta <> nil;
begin
CGContextSetFillColorWithColor(cg, lForegroundColor.CGColor);
GlyphCount := CTLineGetGlyphCount(CoreLine);
SetLength(Locations, GlyphCount);
Locations[0].x := 0;
Locations[0].y := 0;
for k := 1 to GlyphCount - 1 do
begin
Locations[k] := Locations[k - 1];
Locations[k].x := Locations[k].x + CharsDelta[k - 1]
end;
Runs := CTLineGetGlyphRuns(CoreLine);
if Runs <> nil then
begin
GlyphCount := 0;
RunCount := CFArrayGetCount(Runs);
for i := 0 to RunCount - 1 do
begin
CurRun := CTRunRef(CFArrayGetValueAtIndex(Runs, i));
CurGlyphCount := CTRunGetGlyphCount(CurRun);
SetLength(Glyphs, CurGlyphCount);
CTRunGetGlyphs(CurRun, CFRangeMake(0,0), @Glyphs[0]);
RunFont := CFDictionaryGetValue(CTRunGetAttributes(CurRun), kCTFontAttributeName);
CTFontDrawGlyphs(runFont, @Glyphs[0], @Locations[GlyphCount], CurGlyphCount, cg);
GlyphCount := GlyphCount + CurGlyphCount;
end;
end
end;
if cfs_Strikeout in FFont.Style then begin
CGContextSetStrokeColorWithColor(cg, lForegroundColor.CGColor);
StrikeH := Font.Font.xHeight / 2;
StrikeW := CTLineGetTypographicBounds(CoreLine, nil, nil, nil);
CGContextMoveToPoint(cg, X, YPrime + StrikeH);
CGContextAddLineToPoint(cg, X + StrikeW, YPrime + StrikeH);
CGContextStrokePath(cg);
end;
finally
CFRelease(CoreLine);
end;
finally
CFRelease(AttribStr);
end;
finally
Str.release;
end;
end;
CGContextRestoreGState(CGContext());
CGContextRestoreGState(cg);
AttachedBitmap_SetModified();
end;
@ -2411,30 +2196,32 @@ end;
function TCocoaContext.GetTextExtentPoint(AStr: PChar; ACount: Integer; var Size: TSize): Boolean;
var
s : NSString;
M : NSMutableAttributedString;
AttribStr : CFAttributedStringRef;
CoreLine : CTLineRef;
r : NSRect;
begin
{FText.SetText(AStr, ACount);
Size := FText.GetSize;
Result := True;}
S := NSString( CFStringCreateWithBytesNoCopy(nil, AStr, ACount, kCFStringEncodingUTF8,
false,
kCFAllocatorNull));
S := NSStringUtf8(AStr, ACount);
Result := Assigned(S);
if not Result then Exit;
M := NSMutableAttributedString.alloc.initWithString(S);
Result := Assigned(M);
AttribStr := CFAttributedStringCreate(kCFAllocatorDefault, CFStringRef(S),
CFDictionaryRef(NSDictionary.dictionaryWithObject_forKey(
Font.Font, id(kCTFontAttributeName))));
Result := Assigned(AttribStr);
if Result then
begin
FText.SetFontToStr(M);
r := M.boundingRectWithSize_options(NSMakeSize(MaxInt, MaxInt), 0);
Size.cx := Round(r.size.width);
Size.cy := Round(r.Size.height);
M.release;
CoreLine := CTLineCreateWithAttributedString(CFAttributedStringRef(AttribStr));
try
r := CTLineGetBoundsWithOptions(CoreLine, 0);
Size.cx := Round(r.size.width);
Size.cy := Round(r.Size.height);
finally
CFRelease(CoreLine);
end;
end;
CFRelease(S);
S.release;
end;
{------------------------------------------------------------------------------
@ -2446,11 +2233,9 @@ end;
------------------------------------------------------------------------------}
function TCocoaContext.GetTextMetrics(var TM: TTextMetric): Boolean;
var
Glyphs: TGlyphArray;
Adjustments: array of NSSize;
I: Integer;
A: Single;
lNSFont: NSFont;
AttrStr: CFAttributedStringRef;
CoreLine: CTLineRef;
begin
result := False;
if not Assigned(Font) then
@ -2467,21 +2252,22 @@ begin
TM.tmExternalLeading := 0;
TM.tmMaxCharWidth := Round(lNSFont.maximumAdvancement.width);
FText.SetText('WMTigq[_|^', 10);
Glyphs := FText.GetGlyphs;
if Length(Glyphs) > 0 then
begin
SetLength(Adjustments, Length(Glyphs));
lNSFont.getAdvancements_forGlyphs_count(@Adjustments[0], @Glyphs[0], Length(Glyphs));
A := 0;
for I := 0 to High(Adjustments) do
A := A + Adjustments[I].width;
TM.tmAveCharWidth := Round(A / Length(Adjustments));
SetLength(Adjustments, 0);
SetLength(Glyphs, 0);
end
else
TM.tmAveCharWidth := TM.tmMaxCharWidth;
AttrStr := CFAttributedStringCreate(kCFAllocatorDefault,
CFSTR('WMTigq[_|^'),
CFDictionaryRef(NSDictionary.dictionaryWithObject_forKey(
Font.Font, id(kCTFontAttributeName))));
try
CoreLine := CTLineCreateWithAttributedString(CFAttributedStringRef(AttrStr));
try
TM.tmAveCharWidth := Round(CTLineGetTypographicBounds(CoreLine, nil, nil, nil) /
CTLineGetGlyphCount(CoreLine));
finally
CFRelease(CoreLine);
end;
finally
CFRelease(AttrStr);
end;
TM.tmOverhang := 0;
TM.tmDigitizedAspectX := 0;

View File

@ -46,7 +46,7 @@ function NSScreenZeroHeight: CGFloat;
function CreateParamsToNSRect(const params: TCreateParams): NSRect;
function NSStringUtf8(s: PChar): NSString;
function NSStringUtf8(s: PChar; len: Integer = -1): NSString;
function NSStringUtf8(const s: String): NSString;
function NSStringToString(ns: NSString): String;
@ -773,14 +773,24 @@ begin
with params do Result:=GetNSRect(X,Y,Width,Height);
end;
function NSStringUtf8(s: PChar): NSString;
function NSStringUtf8(s: PChar; len: Integer = -1): NSString;
var
cf: CFStringRef;
r: Integer;
W: WideString;
WLen: Integer;
begin
{NSString and CFStringRef are interchangable}
cf := CFStringCreateWithCString(nil, S, kCFStringEncodingUTF8);
Result := NSString(cf);
// Avoid CFString/NSString UTF8 functions (see other overload)
Result := nil;
if len = -1 then
len := StrLen(s);
if len > 0 then
begin
SetLength(W, len);
WLen := Utf8ToUnicode(PWideChar(W), Length(W) + 1, s, len) - 1;
if WLen > 0 then
Result := NSString.alloc.initWithCharacters_length(unicharPtr(W), WLen);
end;
if Result = nil then
Result := NSString.alloc.init;
end;
function NSStringUtf8(const s: String): NSString;