First Commit
This commit is contained in:
commit
ae0fab9400
424 files changed
+71006
No files matched your search
@@ -0,0 +1,314 @@
|
||||
// 2 january 2017
|
||||
#import "uipriv_darwin.h"
|
||||
#import "draw.h"
|
||||
|
||||
// TODO on an empty string nLines == 0
|
||||
// we must prevent this somehow
|
||||
// TODO in general, every function could be more robust, but we cannot have a situation where there are zero lines
|
||||
// TODO what happens to extents if only whitespace?
|
||||
|
||||
struct uiDrawTextLayout {
|
||||
CFAttributedStringRef attrstr;
|
||||
|
||||
// the width as passed into uiDrawTextLayout constructors
|
||||
double width;
|
||||
|
||||
CTFramesetterRef framesetter;
|
||||
|
||||
// the *actual* size of the frame
|
||||
// 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
|
||||
// (this I confirmed through experimentation)
|
||||
// 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)
|
||||
CGSize size;
|
||||
|
||||
CGPathRef path;
|
||||
CTFrameRef frame;
|
||||
|
||||
CFArrayRef lines;
|
||||
CFIndex nLines;
|
||||
// we compute this once when first creating the layout
|
||||
uiDrawTextLayoutLineMetrics *lineMetrics;
|
||||
|
||||
NSArray *backgroundBlocks;
|
||||
|
||||
// for converting CFAttributedString indices from/to byte offsets
|
||||
size_t *u8tou16;
|
||||
size_t nUTF8;
|
||||
size_t *u16tou8;
|
||||
size_t nUTF16;
|
||||
};
|
||||
|
||||
// TODO document that lines may or may not overlap because ours do in the case of multiple combining characters
|
||||
static uiDrawTextLayoutLineMetrics *computeLineMetrics(CTFrameRef frame, CGSize size)
|
||||
{
|
||||
uiDrawTextLayoutLineMetrics *metrics;
|
||||
CFArrayRef lines;
|
||||
CTLineRef line;
|
||||
CFIndex i, n;
|
||||
CGFloat ypos;
|
||||
CGRect bounds, boundsNoLeading;
|
||||
CGFloat ascent, descent, leading;
|
||||
CGPoint *origins;
|
||||
|
||||
lines = CTFrameGetLines(frame);
|
||||
n = CFArrayGetCount(lines);
|
||||
metrics = (uiDrawTextLayoutLineMetrics *) uiAlloc(n * sizeof (uiDrawTextLayoutLineMetrics), "uiDrawTextLayoutLineMetrics[] (text layout)");
|
||||
|
||||
origins = (CGPoint *) uiAlloc(n * sizeof (CGPoint), "CGPoint[] (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;
|
||||
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;
|
||||
}
|
||||
|
||||
uiFree(origins);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
uiDrawTextLayout *uiDrawNewTextLayout(uiDrawTextLayoutParams *p)
|
||||
{
|
||||
uiDrawTextLayout *tl;
|
||||
CGFloat cgwidth;
|
||||
CFRange range, unused;
|
||||
CGRect rect;
|
||||
|
||||
tl = uiNew(uiDrawTextLayout);
|
||||
tl->attrstr = attrstrToCoreFoundation(p, &(tl->backgroundBlocks));
|
||||
range.location = 0;
|
||||
range.length = CFAttributedStringGetLength(tl->attrstr);
|
||||
tl->width = p->Width;
|
||||
|
||||
// TODO kCTParagraphStyleSpecifierMaximumLineSpacing, kCTParagraphStyleSpecifierMinimumLineSpacing, kCTParagraphStyleSpecifierLineSpacingAdjustment for line spacing
|
||||
tl->framesetter = CTFramesetterCreateWithAttributedString(tl->attrstr);
|
||||
if (tl->framesetter == NULL) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
cgwidth = (CGFloat) (tl->width);
|
||||
if (cgwidth < 0)
|
||||
cgwidth = CGFLOAT_MAX;
|
||||
tl->size = CTFramesetterSuggestFrameSizeWithConstraints(tl->framesetter,
|
||||
range,
|
||||
// TODO kCTFramePathWidthAttributeName?
|
||||
NULL,
|
||||
CGSizeMake(cgwidth, CGFLOAT_MAX),
|
||||
&unused); // not documented as accepting NULL (TODO really?)
|
||||
|
||||
rect.origin = CGPointZero;
|
||||
rect.size = tl->size;
|
||||
tl->path = CGPathCreateWithRect(rect, NULL);
|
||||
tl->frame = CTFramesetterCreateFrame(tl->framesetter,
|
||||
range,
|
||||
tl->path,
|
||||
// TODO kCTFramePathWidthAttributeName?
|
||||
NULL);
|
||||
if (tl->frame == NULL) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
tl->lines = CTFrameGetLines(tl->frame);
|
||||
tl->nLines = CFArrayGetCount(tl->lines);
|
||||
tl->lineMetrics = computeLineMetrics(tl->frame, tl->size);
|
||||
|
||||
// and finally copy the UTF-8/UTF-16 conversion tables
|
||||
tl->u8tou16 = attrstrCopyUTF8ToUTF16(p->String, &(tl->nUTF8));
|
||||
tl->u16tou8 = attrstrCopyUTF16ToUTF8(p->String, &(tl->nUTF16));
|
||||
|
||||
return tl;
|
||||
}
|
||||
|
||||
void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
|
||||
{
|
||||
uiFree(tl->u16tou8);
|
||||
uiFree(tl->u8tou16);
|
||||
[tl->backgroundBlocks release];
|
||||
uiFree(tl->lineMetrics);
|
||||
CFRelease(tl->frame);
|
||||
CFRelease(tl->path);
|
||||
CFRelease(tl->framesetter);
|
||||
CFRelease(tl->attrstr);
|
||||
uiFree(tl);
|
||||
}
|
||||
|
||||
// TODO document that (x,y) is the top-left corner of the *entire frame*
|
||||
void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
|
||||
{
|
||||
backgroundBlock b;
|
||||
CGAffineTransform textMatrix;
|
||||
|
||||
CGContextSaveGState(c->c);
|
||||
// save the text matrix because it's not part of the graphics state
|
||||
textMatrix = CGContextGetTextMatrix(c->c);
|
||||
|
||||
for (b in tl->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 - tl->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(tl->frame, c->c);
|
||||
|
||||
CGContextSetTextMatrix(c->c, textMatrix);
|
||||
CGContextRestoreGState(c->c);
|
||||
}
|
||||
|
||||
// TODO document that the width and height of a layout is not necessarily the sum of the widths and heights of its constituent lines
|
||||
// TODO width doesn't include trailing whitespace...
|
||||
// TODO figure out how paragraph spacing should play into this
|
||||
void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height)
|
||||
{
|
||||
*width = tl->size.width;
|
||||
*height = tl->size.height;
|
||||
}
|
||||
|
||||
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
|
||||
{
|
||||
return tl->nLines;
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
|
||||
{
|
||||
CTLineRef lr;
|
||||
CFRange range;
|
||||
|
||||
lr = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, line);
|
||||
range = CTLineGetStringRange(lr);
|
||||
*start = tl->u16tou8[range.location];
|
||||
*end = tl->u16tou8[range.location + range.length];
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
|
||||
{
|
||||
*m = tl->lineMetrics[line];
|
||||
}
|
||||
|
||||
// in the case of overlapping lines, we read lines first to last and use their bottommost point (Y + Height) to determine where the next line should start for hit-testing
|
||||
// TODO should we document this?
|
||||
void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, size_t *pos, int *line)
|
||||
{
|
||||
int i;
|
||||
CTLineRef ln;
|
||||
CFIndex p;
|
||||
|
||||
for (i = 0; i < tl->nLines; i++) {
|
||||
double ltop, lbottom;
|
||||
|
||||
ltop = tl->lineMetrics[i].Y;
|
||||
lbottom = ltop + tl->lineMetrics[i].Height;
|
||||
// y will already >= ltop at this point since the past lbottom should == (or at least >=, see above) ltop
|
||||
if (y < lbottom)
|
||||
break;
|
||||
}
|
||||
if (i == tl->nLines)
|
||||
i--;
|
||||
*line = i;
|
||||
|
||||
ln = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, i);
|
||||
// note: according to the docs, we pass a y of 0 for this since the is the baseline of that line (the point is relative to the line)
|
||||
// note: x is relative to the line origin
|
||||
x -= tl->lineMetrics[*line].X;
|
||||
p = CTLineGetStringIndexForPosition(ln, CGPointMake(x, 0));
|
||||
if (p == kCFNotFound) {
|
||||
// TODO
|
||||
}
|
||||
*pos = tl->u16tou8[p];
|
||||
}
|
||||
|
||||
double uiDrawTextLayoutByteLocationInLine(uiDrawTextLayout *tl, size_t pos, int line)
|
||||
{
|
||||
CTLineRef lr;
|
||||
CFRange range;
|
||||
|
||||
pos = tl->u8tou16[pos];
|
||||
if (line < 0 || line >= tl->nLines)
|
||||
return -1;
|
||||
lr = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, line);
|
||||
range = CTLineGetStringRange(lr);
|
||||
// note: >, not >=, because the position at end is valid!
|
||||
if (pos < range.location || pos > (range.location + range.length))
|
||||
return -1;
|
||||
// no point in checking the return; we already validated everything and 0 is a valid return for the first index :/
|
||||
// note: the result is relative to the line origin (TODO find documentation to support this)
|
||||
// TODO document that these functions do this
|
||||
return CTLineGetOffsetForStringIndex(lr, pos, NULL) + tl->lineMetrics[line].X;
|
||||
}
|
||||
|
||||
void caretDrawParams(uiDrawContext *c, double height, struct caretDrawParams *p)
|
||||
{
|
||||
NSColor *cc;
|
||||
CGFloat cr, cg, cb, ca;
|
||||
|
||||
// Interface Builder sets this as the insertion point color for a NSTextView by default
|
||||
cc = [NSColor controlTextColor];
|
||||
// the given color may not be an RGBA color, which will cause the -getRed:green:blue:alpha: call to throw an exception
|
||||
cc = [cc colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
|
||||
[cc getRed:&cr green:&cg blue:&cb alpha:&ca];
|
||||
p->r = cr;
|
||||
p->g = cg;
|
||||
p->b = cb;
|
||||
p->a = ca;
|
||||
// both cc and the controlTextColor it was made from will be autoreleased since they aren't new or init calls
|
||||
// TODO disabled carets have some blending applied...
|
||||
|
||||
// okay there doesn't seem to be any defined metrics for these, argh...
|
||||
p->width = 1;
|
||||
p->xoff = 0;
|
||||
}
|
||||
Reference in new issue
Block a user