libui/darwin/drawtext.m

308 lines
9.1 KiB
Mathematica
Raw Normal View History

// 2 january 2017
#import "uipriv_darwin.h"
#import "draw.h"
struct uiDrawTextLayout {
CFAttributedStringRef attrstr;
double width;
2017-01-03 22:59:23 -06:00
CTFramesetterRef framesetter;
// 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)
2017-01-03 22:59:23 -06:00
CGSize size;
CGPathRef path;
CTFrameRef frame;
CFArrayRef lines;
2017-01-07 19:09:44 -06:00
size_t *u16tou8;
size_t nu16tou8; // TODO I don't like the casing of this name
};
static CTFontRef fontdescToCTFont(uiDrawFontDescriptor *fd)
{
CTFontDescriptorRef desc;
CTFontRef font;
2017-01-03 22:59:23 -06:00
desc = fontdescToCTFontDescriptor(fd);
font = CTFontCreateWithFontDescriptor(desc, fd->Size, NULL);
CFRelease(desc); // TODO correct?
return font;
}
static CFAttributedStringRef attrstrToCoreFoundation(uiAttributedString *s, uiDrawFontDescriptor *defaultFont)
{
CFStringRef cfstr;
CFMutableDictionaryRef defaultAttrs;
CTFontRef defaultCTFont;
CFAttributedStringRef base;
CFMutableAttributedStringRef mas;
cfstr = CFStringCreateWithCharacters(NULL, attrstrUTF16(s), attrstrUTF16Len(s));
if (cfstr == NULL) {
// TODO
}
defaultAttrs = CFDictionaryCreateMutable(NULL, 1,
&kCFCopyStringDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
if (defaultAttrs == NULL) {
// TODO
}
defaultCTFont = fontdescToCTFont(defaultFont);
CFDictionaryAddValue(defaultAttrs, kCTFontAttributeName, defaultCTFont);
CFRelease(defaultCTFont);
base = CFAttributedStringCreate(NULL, cfstr, defaultAttrs);
if (base == NULL) {
// TODO
}
CFRelease(cfstr);
CFRelease(defaultAttrs);
mas = CFAttributedStringCreateMutableCopy(NULL, 0, base);
CFRelease(base);
CFAttributedStringBeginEditing(mas);
// TODO copy in the attributes
CFAttributedStringEndEditing(mas);
return mas;
}
uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width)
{
2017-01-03 22:59:23 -06:00
uiDrawTextLayout *tl;
CGFloat cgwidth;
CFRange range, unused;
CGRect rect;
tl = uiNew(uiDrawTextLayout);
tl->attrstr = attrstrToCoreFoundation(s, defaultFont);
range.location = 0;
range.length = CFAttributedStringGetLength(tl->attrstr);
tl->width = width;
// TODO CTFrameProgression for RTL/LTR
// TODO kCTParagraphStyleSpecifierMaximumLineSpacing, kCTParagraphStyleSpecifierMinimumLineSpacing, kCTParagraphStyleSpecifierLineSpacingAdjustment for line spacing
tl->framesetter = CTFramesetterCreateWithAttributedString(tl->attrstr);
if (tl->framesetter == NULL) {
// TODO
}
cgwidth = (CGFloat) width;
if (cgwidth < 0)
cgwidth = CGFLOAT_MAX;
// TODO these seem to be floor()'d or truncated?
// TODO double check to make sure this TODO was right
tl->size = CTFramesetterSuggestFrameSizeWithConstraints(tl->framesetter,
range,
// TODO kCTFramePathWidthAttributeName?
NULL,
CGSizeMake(cgwidth, CGFLOAT_MAX),
&unused); // not documented as accepting NULL
rect.origin = CGZeroPoint;
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);
2017-01-07 19:09:44 -06:00
// and finally copy the UTF-16 to UTF-8 index conversion table
tl->u16tou8 = attrstrCopyUTF16ToUTF8(s, &(tl->nu16tou8));
2017-01-03 22:59:23 -06:00
return tl;
}
void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
{
2017-01-07 19:09:44 -06:00
uiFree(tl->u16tou8);
// TODO release tl->lines?
CFRelease(tl->frame);
2017-01-03 22:59:23 -06:00
CFRelease(tl->path);
CFRelease(tl->framesetter);
CFRelease(tl->attrstr);
uiFree(tl);
}
// TODO double-check helvetica
// TODO document that (x,y) is the top-left corner of the *entire frame*
void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
{
CGContextSaveGState(c->c);
// 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, cheight);
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
y = c->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);
CGContextRestoreGState(c->c);
}
2017-01-05 16:56:47 -06:00
// TODO document that the width and height of a layout is not necessarily the sum of the widths and heights of its constituent lines; this is definitely untrue on OS X, where lines are placed in such a way that the distance between baselines is always integral
void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height)
{
*width = tl->size.width;
*height = tl->size.height;
}
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
{
return CFArrayGetCount(tl->lines);
}
2016-01-15 21:48:38 -06:00
// 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)
{
CTLineRef lr;
CFRange range;
lr = getline(tl, line);
range = CTLineGetStringRange(lr);
2017-01-07 19:09:44 -06:00
*start = tl->u16tou8[range.location];
*end = tl->u16tou8[range.location + range.length];
}
void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
{
CTLineRef lr;
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)
{
}
2017-01-07 19:09:44 -06:00
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)
2016-05-23 00:11:43 -05:00
{
2017-01-07 19:09:44 -06:00
CGPoint *mkLineOrigins;
CFIndex i, n;
CTLineRef line;
double firstYForLine;
CGFloat width, NULL, descent, leading;
CFRange range;
n = CFArrayGetCount(tl->lines);
if (n == 0) {
// TODO fill result
return;
}
origins = mkLineOrigins(tl);
if (y < 0) {
line = getline(tl, 0);
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;
firstYForLine = origins[i].y + descent + leading;
}
}
if (i == n) {
i--;
result->Line = i;
result->YPosition = uiDrawTextLayoutHitTestPositionAfter;
} else {
result->Line = i;
result->YPosition = uiDrawTextLayoutHitTestPositionInside;
if (i == 0 && y < 0)
result->YPosition = uiDrawTextLayoutHitTestPositionBefore;
}
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];
// TODO compute the fractional offset
result->InTrailingWhitespace = x < origins[i].x || x >= (origins[i].x + width);
}
uiFree(origins);
2016-05-23 00:11:43 -05:00
}
void uiDrawTextLayoutByteRangeToRectangle(uiDrawTextLayout *tl, size_t start, size_t end, uiDrawTextLayoutByteRangeRectangle *r)
{
}