And rewrote drawtext.m based around the new Core Text research.

This commit is contained in:
Pietro Gagliardi 2017-01-17 12:02:42 -05:00
parent e63a42a290
commit 794d30154c
2 changed files with 133 additions and 107 deletions

View File

@ -2,19 +2,33 @@
#import "uipriv_darwin.h" #import "uipriv_darwin.h"
#import "draw.h" #import "draw.h"
// TODO what happens if nLines == 0 in any function?
struct uiDrawTextLayout { struct uiDrawTextLayout {
CFAttributedStringRef attrstr; CFAttributedStringRef attrstr;
// the width as passed into uiDrawTextLayout constructors
double width; double width;
CTFramesetterRef framesetter; CTFramesetterRef framesetter;
// the *actual* size of the frame
// note: technically, metrics returned from frame are relative to CGPathGetPathBoundingBox(tl->path) // note: technically, metrics returned from frame are relative to CGPathGetPathBoundingBox(tl->path)
// however, from what I can gather, for a path created by CGPathCreateWithRect(), like we do (with a NULL transform), CGPathGetPathBoundingBox() seems to just return the standardized form of the rect used to create the path // however, from what I can gather, for a path created by CGPathCreateWithRect(), like we do (with a NULL transform), CGPathGetPathBoundingBox() seems to just return the standardized form of the rect used to create the path
// (this I confirmed through experimentation) // (this I confirmed through experimentation)
// so we can just use tl->size for adjustments // so we can just use tl->size for adjustments
// we don't need to adjust coordinates by any origin since our rect origin is (0, 0) // we don't need to adjust coordinates by any origin since our rect origin is (0, 0)
CGSize size; CGSize size;
CGPathRef path; CGPathRef path;
CTFrameRef frame; CTFrameRef frame;
CFArrayRef lines; CFArrayRef lines;
CFIndex nLines;
// we compute this once when first creating the layout
uiDrawTextLayoutLineMetrics *lineMetrics;
// for converting CFAttributedString indices to byte offsets
size_t *u16tou8; size_t *u16tou8;
size_t nu16tou8; // TODO I don't like the casing of this name size_t nu16tou8; // TODO I don't like the casing of this name
}; };
@ -68,6 +82,77 @@ static CFAttributedStringRef attrstrToCoreFoundation(uiAttributedString *s, uiDr
return mas; return mas;
} }
static uiDrawTextLayoutLineMetrics *computeLineMetrics(CTFrame frame, CGSize size)
{
uiDrawTextLayoutLineMetrics *metrics;
CFArray lines;
CTLineRef line;
CFIndex i, n;
CGFloat ypos;
CGRect bounds, boundsNoLeading;
CGFloat ascent, descent, leading;
CGPoint *origins;
lines = CTFrameGetLines(frame);
n = CFArrayGetCount(lines);
metrics = (uiDrawTextLay
outLineMetrics *) uiAlloc(n * sizeof (uiDrawTextLayoutLineMetrics), "uiDrawTextLayoutLineMetrics[] (text layout)");
origins = (CGFloat *) uiAlloc(n * sizeof (CGFloat), "CGFloat[] (text layout)");
CTFrameGetLineOrigins(frame, CFRangeMake(0, n), origins);
ypos = size.height;
for (i = 0; i < n; i++) {
line = (CTLineRef) CFArrayGetValueAtIndex(lines, i);
bounds = CTLineGetBoundsWithOptions(line, 0);
boundsNoLeading = CTLineGetBoundsWithOptions(line, kCTLineBoundsExcludeTypographicLeading);
// this is equivalent to boundsNoLeading.size.height + boundsNoLeading.origin.y (manually verified)
ascent = bounds.size.height + bounds.origin.y;
descent = -boundsNoLeading.origin.y;
// TODO does this preserve leading sign?
leading = -bounds.origin.y - descent;
// Core Text always rounds these up for paragraph style calculations; there is a flag to control it but it's inaccessible (and this behavior is turned off for old versions of iPhoto)
ascent = floor(ascent + 0.5);
descent = floor(descent + 0.5);
if (leading > 0)
leading = floor(leading + 0.5);
metrics[i].X = origins[i].x;
metrics[i].Y = origins[i].y - descent - leading;
metrics[i].Width = bounds.size.width;
metrics[i].Height = ascent + descent + leading;
metrics[i].BaselineY = origins[i].y;
metrics[i].Ascent = ascent;
metrics[i].Descent = descent;
metrics[i].Leading = leading;
// TODO
metrics[i].ParagraphSpacingBefore = 0;
metrics[i].LineHeightSpace = 0;
metrics[i].LineSpacing = 0;
metrics[i].ParagraphSpacing = 0;
// and finally advance to the next line
ypos += metrics[i].Height;
}
// okay, but now all these metrics are unflipped
// we need to flip them
for (i = 0; i < n; i++) {
metrics[i].Y = size.height - metrics[i].Y;
// go from bottom-left corner to top-left
metrics[i].Y -= metrics[i].Height;
metrics[i].BaselineY = size.height - metrics[i].BaselineY;
// TODO also adjust by metrics[i].Height?
}
uiFree(origins);
return metrics;
}
uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width) uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width)
{ {
uiDrawTextLayout *tl; uiDrawTextLayout *tl;
@ -113,6 +198,8 @@ uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescripto
} }
tl->lines = CTFrameGetLines(tl->frame); tl->lines = CTFrameGetLines(tl->frame);
tl->nLines = CFArrayGetCount(tl->lines);
tl->lineMetrics = computeLineMetrics(tl->frame, tl->size);
// and finally copy the UTF-16 to UTF-8 index conversion table // and finally copy the UTF-16 to UTF-8 index conversion table
tl->u16tou8 = attrstrCopyUTF16ToUTF8(s, &(tl->nu16tou8)); tl->u16tou8 = attrstrCopyUTF16ToUTF8(s, &(tl->nu16tou8));
@ -123,6 +210,7 @@ uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescripto
void uiDrawFreeTextLayout(uiDrawTextLayout *tl) void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
{ {
uiFree(tl->u16tou8); uiFree(tl->u16tou8);
uiFree(tl->lineMetrics);
// TODO release tl->lines? // TODO release tl->lines?
CFRelease(tl->frame); CFRelease(tl->frame);
CFRelease(tl->path); CFRelease(tl->path);
@ -131,7 +219,6 @@ void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
uiFree(tl); uiFree(tl);
} }
// TODO double-check helvetica
// TODO document that (x,y) is the top-left corner of the *entire frame* // TODO document that (x,y) is the top-left corner of the *entire frame*
void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y) void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
{ {
@ -167,18 +254,15 @@ void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl) int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
{ {
return CFArrayGetCount(tl->lines); return tl->nLines;
} }
// TODO release when done?
#define getline(tl, line) ((CTLineRef) CFArrayGetValueAtIndex(tl->lines, line))
void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end) void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
{ {
CTLineRef lr; CTLineRef lr;
CFRange range; CFRange range;
lr = getline(tl, line); lr = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, line);
range = CTLineGetStringRange(lr); range = CTLineGetStringRange(lr);
*start = tl->u16tou8[range.location]; *start = tl->u16tou8[range.location];
*end = tl->u16tou8[range.location + range.length]; *end = tl->u16tou8[range.location + range.length];
@ -186,120 +270,60 @@ void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start
void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m) void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
{ {
CTLineRef lr; *m = tl->lineMetrics[line];
CFRange range;
CGPoint origin;
CGFloat ascent, descent, leading;
range.location = line;
range.length = 1;
CTFrameGetLineOrigins(tl->frame, range, &origin);
m->X = origin.x;
// and remember that the frame is flipped
m->BaselineY = tl->size.height - origin.y;
lr = getline(tl, line);
// though CTLineGetTypographicBounds() returns 0 on error, it also returns 0 on an empty string, so we can't reasonably check for error
m->Width = CTLineGetTypographicBounds(lr, &ascent, &descent, &leading);
m->Ascent = ascent;
m->Descent = descent;
m->Leading = leading;
} }
void uiDrawTextLayoutByteIndexToGraphemeRect(uiDrawTextLayout *tl, size_t pos, int *line, double *x, double *y, double *width, double *height) void uiDrawTextLayoutByteIndexToGraphemeRect(uiDrawTextLayout *tl, size_t pos, int *line, double *x, double *y, double *width, double *height)
{ {
} }
static CGPoint *mkLineOrigins(uiDrawTextLayout *tl)
{
CGPoint *origins;
CFRange range;
CFIndex i, n;
CTLine line;
CGFloat ascent;
n = CFArrayGetCount(tl->lines);
range.location = 0;
range.length = n;
origins = (CGPoint *) uiAlloc(n * sizeof (CGPoint), "CGPoint[]");
CTFrameGetLineOrigins(tl->frame, range, origins);
for (i = 0; i < n; i++) {
line = getline(tl, i);
CTLineGetTypographicBounds(line, &ascent, NULL, NULL);
origins[i].y = tl->size.height - (origins[i].y + ascent);
}
return origins;
}
void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, uiDrawTextLayoutHitTestResult *result) void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, uiDrawTextLayoutHitTestResult *result)
{ {
CGPoint *mkLineOrigins; CFIndex i;
CFIndex i, n; CTLine line;
CTLineRef line; CFIndex pos;
double firstYForLine; CGFloat charLeft, charRight;
CGFloat width, NULL, descent, leading;
CFRange range;
n = CFArrayGetCount(tl->lines); if (y >= 0) {
if (n == 0) { for (i = 0; i < tl->nLines; i++) {
// TODO fill result double ltop, lbottom;
return;
}
origins = mkLineOrigins(tl); ltop = tl->lineMetrics[i].Y;
if (y < 0) { lbottom = ltop + tl->lineMetrics[i].Height;
line = getline(tl, 0); if (y >= ltop && y < lbottom)
width = CTLineGetTypographicBounds(line, NULL, NULL, NULL);
i = 0;
} else {
firstYForLine = 0;
for (i = 0; i < n; i++) {
line = getline(tl, i);
width = CTLineGetTypographicBounds(line, NULL, &descent, &leading);
if (y < maxYForLine)
break; break;
firstYForLine = origins[i].y + descent + leading;
} }
}
if (i == n) {
i--;
result->Line = i;
result->YPosition = uiDrawTextLayoutHitTestPositionAfter;
} else {
result->Line = i;
result->YPosition = uiDrawTextLayoutHitTestPositionInside; result->YPosition = uiDrawTextLayoutHitTestPositionInside;
if (i == 0 && y < 0) if (i == tl->nLines) {
result->YPosition = uiDrawTextLayoutHitTestPositionBefore; i--;
} result->YPosition = uiDrawTextLayoutHitTestPositionAfter;
result->InTrailingWhitespace = 0;
range = CTLineGetStringRange(line);
if (x < 0) {
result->Start = tl->u16tou8[range.location];
result->End = result->Start;
result->XPosition = uiDrawTextLayoutHitTestPositionBefore;
} else if (x > tl->size.width) {
result->Start = tl->u16tou8[range.location + range.length];
result->End = result->Start;
result->XPosition = uiDrawTextLayoutHitTestPositionAfter;
} else {
CGPoint pos;
CFIndex index;
result->XPosition = uiDrawTextLa
youtHitTestPositionInside;
pos.x = x;
// TODO this isn't set properly in any of the fast-track cases
pos.y = y - firstYForLine;
index = CTLineGetStringIn
dexForPosition(line, pos);
if (index == kCFNotFound) {
// TODO
} }
result->Pos = tl->u16tou8[index]; } else {
// TODO compute the fractional offset i = 0;
result->InTrailingWhitespace = x < origins[i].x || x >= (origins[i].x + width); result->YPosition = uiDrawTextLayoutHitTestPositionBefore;
} }
uiFree(origins); m->Line = i;
result->XPosition = uiDrawTextLayoutHitTestPositionInside;
if (x < tl->lineMetrics[i].X) {
result->XPosition = uiDrawTextLay
outHitTestPositionBefore;
// and forcibly return the first character
x = tl->lineMetrics[i].X;
} else if (x > (tl->lineMetrics[i].X + tl->lineMetrics[i].Width)) {
result->XPosition = uiDrawTextLayoutHitTestP
ositionAfter;
// and forcibly return the last character
x = tl->lineMetrics[i].X + tl->lineMetrics[i].Width;
}
line = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, i);
pos = CTLineGetStringIndexForPosition(line, CGP
ointMake(x, 0));
if (pos == kCFNotFound) {
// TODO
}
m->Pos = tl->u16tou8[pos];
} }
void uiDrawTextLayoutByteRangeToRectangle(uiDrawTextLayout *tl, size_t start, size_t end, uiDrawTextLayoutByteRangeRectangle *r) void uiDrawTextLayoutByteRangeToRectangle(uiDrawTextLayout *tl, size_t start, size_t end, uiDrawTextLayoutByteRangeRectangle *r)

View File

@ -132,8 +132,10 @@ struct uiDrawTextLayoutHitTestResult {
int Line; int Line;
uiDrawTextLayoutHitTestPosition XPosition; uiDrawTextLayoutHitTestPosition XPosition;
uiDrawTextLayoutHitTestPosition YPosition; uiDrawTextLayoutHitTestPosition YPosition;
int InTrailingWhitespace; // TODO?
double XFraction; // int InTrailingWhitespace;
// TODO?
// double XFraction;
}; };
struct uiDrawTextLayoutByteRangeRectangle { struct uiDrawTextLayoutByteRangeRectangle {