And finished up drawtext.m for now.

This commit is contained in:
Pietro Gagliardi 2018-03-08 22:38:53 -05:00
parent 7451d455e5
commit 5535c43bd8
1 changed files with 154 additions and 71 deletions

View File

@ -3,125 +3,208 @@
#import "draw.h" #import "draw.h"
#import "attrstr.h" #import "attrstr.h"
// problem: for a CTFrame made from an empty string, the CTLine array will be empty, and we will crash when gathering metrics or hit-testing // problem: for a CTFrame made from an empty string, the CTLine array will be empty, and we will crash when doing anything requiring a CTLine
// solution: for those cases, maintain a separate framesetter just for computing those things // solution: for those cases, maintain a separate framesetter just for computing those things
// in the usual case, the separate copy will just be identical to the regular one, with extra references to everything within // in the usual case, the separate copy will just be identical to the regular one, with extra references to everything within
struct frame { @interface uiprivTextFrame {
CFAttributedStringRef attrstr; CFAttributedStringRef attrstr;
NSArray *backgroundBlocks; NSArray *backgroundBlocks;
CTFramesetterRef framesetter; CTFramesetterRef framesetter;
CGSize size; CGSize size;
CGPathRef path; CGPathRef path;
CTFrameRef frame; CTFrameRef frame;
}; }
- (id)initWithLayoutParams:(uiDrawTextLayoutParams *)p;
- (void)draw:(uiDrawContext *)c textLayout:(uiDrawTextLayout *)tl at:(double)x y:(double)y;
- (void)returnWidth:(double *)width height:(double *)height;
- (CFArrayRef)lines;
@end
struct uiDrawTextLayout { @implementation uiprivTextFrame
struct frame forDrawing;
struct frame forMetrics;
};
static void paramsToFrame(uiDrawTextLayoutParams *params, struct frame *frame) - (id)initWithLayoutParams:(uiDrawTextLayoutParams *)p
{ {
CFRange range; CFRange range;
CGFloat width; CGFloat width;
CFRange unused; CFRange unused;
CGRect rect; CGRect rect;
frame->attrstr = uiprivAttributedStringToCFAttributedString(p, &(frame->backgroundBlocks)); self = [super init];
// TODO kCTParagraphStyleSpecifierMaximumLineSpacing, kCTParagraphStyleSpecifierMinimumLineSpacing, kCTParagraphStyleSpecifierLineSpacingAdjustment for line spacing if (self) {
frame->framesetter = CTFramesetterCreateWithAttributedString(tl->attrstr); self->attrstr = uiprivAttributedStringToCFAttributedString(p, &(self->backgroundBlocks));
if (frame->framesetter == NULL) { // TODO kCTParagraphStyleSpecifierMaximumLineSpacing, kCTParagraphStyleSpecifierMinimumLineSpacing, kCTParagraphStyleSpecifierLineSpacingAdjustment for line spacing
// TODO self->framesetter = CTFramesetterCreateWithAttributedString(self->attrstr);
} if (self->framesetter == NULL) {
// TODO
range.location = 0; }
range.length = CFAttributedStringGetLength(tl->attrstr);
range.location = 0;
cgwidth = (CGFloat) (frame->width); range.length = CFAttributedStringGetLength(self->attrstr);
if (cgwidth < 0)
cgwidth = CGFLOAT_MAX; cgwidth = (CGFloat) (p->Width);
frame->size = CTFramesetterSuggestFrameSizeWithConstraints(frame->framesetter, if (cgwidth < 0)
range, cgwidth = CGFLOAT_MAX;
// TODO kCTFramePathWidthAttributeName? self->size = CTFramesetterSuggestFrameSizeWithConstraints(self->framesetter,
NULL, range,
CGSizeMake(cgwidth, CGFLOAT_MAX), // TODO kCTFramePathWidthAttributeName?
&unused); // not documented as accepting NULL (TODO really?) NULL,
CGSizeMake(cgwidth, CGFLOAT_MAX),
rect.origin = CGPointZero; &unused); // not documented as accepting NULL (TODO really?)
rect.size = frame->size;
frame->path = CGPathCreateWithRect(rect, NULL); rect.origin = CGPointZero;
frame->frame = CTFramesetterCreateFrame(tl->framesetter, rect.size = self->size;
range, self->path = CGPathCreateWithRect(rect, NULL);
tl->path, self->frame = CTFramesetterCreateFrame(tl->framesetter,
// TODO kCTFramePathWidthAttributeName? range,
NULL); self->path,
if (frame->frame == NULL) { // TODO kCTFramePathWidthAttributeName?
// TODO NULL);
if (self->frame == NULL) {
// TODO
}
} }
return self;
} }
static void freeFrame(struct frame *frame) - (void)dealloc
{ {
CFRelease(frame->frame); CFRelease(self->frame);
CFRelease(frame->path); CFRelease(self->path);
CFRelease(frame->framesetter); CFRelease(self->framesetter);
[frame->backgroundBlocks release]; [self->backgroundBlocks release];
CFRelease(frame->attrstr); CFRelease(self->attrstr);
[super dealloc];
} }
static void retainFrameCopy(struct frame *out, const struct frame *frame) - (void)draw:(uiDrawContext *)c textLayout:(uiDrawTextLayout *)tl at:(double)x y:(double)y
{ {
memcpy(out, frame, sizeof (struct frame)); backgroundBlock b;
CFRetain(out->attrstr); CGAffineTransform textMatrix;
[out->backgroundBlocks retain];
CFRetain(out->framesetter); CGContextSaveGState(c->c);
CFRetain(out->path); // save the text matrix because it's not part of the graphics state
CFRetain(out->frame); textMatrix = CGContextGetTextMatrix(c->c);
for (b in self->backgroundBlocks)
b(c, tl, x, y);
// Core Text doesn't draw onto a flipped view correctly; we have to pretend it was unflipped
// see the iOS bits of the first example at https://developer.apple.com/library/mac/documentation/StringsTextFonts/Conceptual/CoreText_Programming/LayoutOperations/LayoutOperations.html#//apple_ref/doc/uid/TP40005533-CH12-SW1 (iOS is naturally flipped)
// TODO how is this affected by a non-identity CTM?
CGContextTranslateCTM(c->c, 0, c->height);
CGContextScaleCTM(c->c, 1.0, -1.0);
CGContextSetTextMatrix(c->c, CGAffineTransformIdentity);
// wait, that's not enough; we need to offset y values to account for our new flipping
// TODO explain this calculation
y = c->height - self->size.height - y;
// CTFrameDraw() draws in the path we specified when creating the frame
// this means that in our usage, CTFrameDraw() will draw at (0,0)
// so move the origin to be at (x,y) instead
// TODO are the signs correct?
CGContextTranslateCTM(c->c, x, y);
CTFrameDraw(self->frame, c->c);
CGContextSetTextMatrix(c->c, textMatrix);
CGContextRestoreGState(c->c);
} }
- (void)returnWidth:(double *)width height:(double *)height
{
if (width != NULL)
*width = self->size.width;
if (height != NULL)
*height = self->size.height;
}
- (CFArrayRef)lines
{
return CTFrameGetLines(self->frame);
}
@end
struct uiDrawTextLayout {
uiprivTextFrame *frame;
uiprivTextFrame *forLines;
BOOL empty;
// for converting CFAttributedString indices from/to byte offsets
size_t *u8tou16;
size_t nUTF8;
size_t *u16tou8;
size_t nUTF16;
};
uiDrawTextLayout *uiDrawNewTextLayout(uiDrawTextLayoutParams *p) uiDrawTextLayout *uiDrawNewTextLayout(uiDrawTextLayoutParams *p)
{ {
uiDrawTextLayout *tl; uiDrawTextLayout *tl;
tl = uiprivNew(uiDrawTextLayout); tl = uiprivNew(uiDrawTextLayout);
paramsToFrame(p, &(tl->forDrawing)); tl->frame = [[uiprivTextFrame alloc] initWithLayoutParams:p];
if (uiAttributedStringLength(p->String) != 0) if (uiAttributedStringLength(p->String) != 0)
retainFrameCopy(&(tl->forMetrics), &(tl->forDrawing)); tl->forLines = [tl->frame retain];
else { else {
uiAttributedString *space; uiAttributedString *space;
uiDrawTextLayoutParams p2; uiDrawTextLayoutParams p2;
tl->empty = YES;
space = uiNewAttributedString(" "); space = uiNewAttributedString(" ");
p2 = *p; p2 = *p;
p2.String = space; p2.String = space;
paramsToFrame(&p2, &(tl->forMetrics)); tl->forLines = [[uiprivTextFrame alloc] initWithLayoutParams:&p2];
uiFreeAttributedString(space); uiFreeAttributedString(space);
} }
// and finally copy the UTF-8/UTF-16 conversion tables
tl->u8tou16 = uiprivAttributedStringCopyUTF8ToUTF16Table(p->String, &(tl->nUTF8));
tl->u16tou8 = uiprivAttributedStringCopyUTF16ToUTF8Table(p->String, &(tl->nUTF16));
return tl; return tl;
} }
void uiDrawFreeTextLayout(uiDrawTextLayout *tl) void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
{ {
freeFrame(&(tl->forMetrics)); uiprivFree(tl->u16tou8);
freeFrame(&(tl->forDrawing)); uiprivFree(tl->u8tou16);
[tl->forLines release];
[tl->frame release];
uiprivFree(tl); uiprivFree(tl);
} }
// uiDrawText() draws tl in c with the top-left point of tl at (x, y). // TODO document that (x,y) is the top-left corner of the *entire frame*
_UI_EXTERN void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y); void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
{
[tl->frame draw:c textLayout:tl at:x y:y];
}
// uiDrawTextLayoutExtents() returns the width and height of tl // TODO document that the width and height of a layout is not necessarily the sum of the widths and heights of its constituent lines
// in width and height. The returned width may be smaller than // TODO width doesn't include trailing whitespace...
// the width passed into uiDrawNewTextLayout() depending on // TODO figure out how paragraph spacing should play into this
// how the text in tl is wrapped. Therefore, you can use this // TODO standardize and document the behavior of this on an empty layout
// function to get the actual size of the text layout. void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height)
_UI_EXTERN void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height); {
// TODO explain this, given the above
[tl->frame returnWidth:width height:NULL];
[tl->forLines returnWidth:NULL height:height];
}
// uiDrawTextLayoutNumLines() returns the number of lines in tl. int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
// This number will always be greater than or equal to 1; a text {
// layout with no text only has one line. return CFArrayGetCount([tl->forLines lines]);
_UI_EXTERN int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl); }
// uiDrawTextLayoutLineByteRange() returns the byte indices of the void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
// text that falls into the given line of tl as [start, end). {
_UI_EXTERN void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end); CTLineRef lr;
CFRange range;
lr = (CTLineRef) CFArrayGetValueAtIndex([tl->forLines lines], line);
range = CTLineGetStringRange(lr);
*start = tl->u16tou8[range.location];
if (tl->empty)
*end = *start;
else
*end = tl->u16tou8[range.location + range.length];
}