libui/_wip/attrstr_metrics/darwin_OLD__appkit_drawtext.m

348 lines
11 KiB
Objective-C

// 2 january 2017
#import "uipriv_darwin.h"
#import "draw.h"
@interface lineInfo : NSObject
@property NSRange glyphRange;
@property NSRange characterRange;
@property NSRect lineRect;
@property NSRect lineUsedRect;
@property NSRect glyphBoundingRect;
@property CGFloat baselineOffset;
@property double ascent;
@property double descent;
@property double leading;
@end
@implementation lineInfo
@end
struct uiDrawTextLayout {
// NSTextStorage is subclassed from NSMutableAttributedString
NSTextStorage *attrstr;
NSTextContainer *container;
NSLayoutManager *layoutManager;
// the width as passed into uiDrawTextLayout constructors
double width;
#if 0 /* TODO */
// 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;
#endif
NSMutableArray<lineInfo *> *lineInfo;
// for converting CFAttributedString indices to byte offsets
size_t *u16tou8;
size_t nu16tou8; // TODO I don't like the casing of this name
};
static NSFont *fontdescToNSFont(uiDrawFontDescriptor *fd)
{
NSFontDescriptor *desc;
NSFont *font;
desc = fontdescToNSFontDescriptor(fd);
font = [NSFont fontWithDescriptor:desc size:fd->Size];
[desc release];
return font;
}
static NSTextStorage *attrstrToTextStorage(uiAttributedString *s, uiDrawFontDescriptor *defaultFont)
{
NSString *nsstr;
NSMutableDictionary *defaultAttrs;
NSTextStorage *attrstr;
nsstr = [[NSString alloc] initWithCharacters:attrstrUTF16(s)
length:attrstrUTF16Len(s)];
defaultAttrs = [NSMutableDictionary new];
[defaultAttrs setObject:fontdescToNSFont(defaultFont)
forKey:NSFontAttributeName];
attrstr = [[NSTextStorage alloc] initWithString:nsstr
attributes:defaultAttrs];
[defaultAttrs release];
[nsstr release];
[attrstr beginEditing];
// TODO copy in the attributes
[attrstr endEditing];
return attrstr;
}
// TODO fine-tune all the properties
uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width)
{
uiDrawTextLayout *tl;
CGFloat cgwidth;
// TODO correct type?
NSUInteger index;
tl = uiNew(uiDrawTextLayout);
tl->attrstr = attrstrToTextStorage(s, defaultFont);
tl->width = width;
// TODO the documentation on the size property implies this might not be necessary?
cgwidth = (CGFloat) width;
if (cgwidth < 0)
cgwidth = CGFLOAT_MAX;
// TODO rename to tl->textContainer
tl->container = [[NSTextContainer alloc] initWithSize:NSMakeSize(cgwidth, CGFLOAT_MAX)];
// TODO pull the reference for this
[tl->container setLineFragmentPadding:0];
tl->layoutManager = [[NSLayoutManager alloc] init];
[tl->layoutManager setTypesetterBehavior:NSTypesetterLatestBehavior];
[tl->layoutManager addTextContainer:tl->container];
[tl->attrstr addLayoutManager:tl->layoutManager];
// and force a re-layout (TODO get source
[tl->layoutManager glyphRangeForTextContainer:tl->container];
// TODO equivalent of CTFrameProgression for RTL/LTR?
// now collect line information; see https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html
tl->lineInfo = [NSMutableArray<lineInfo *> new];
index = 0;
while (index < [tl->layoutManager numberOfGlyphs]) {
NSRange glyphRange;
__block lineInfo *li;
li = [lineInfo new];
li.lineRect = [tl->layoutManager lineFragmentRectForGlyphAtIndex:index effectiveRange:&glyphRange];
li.glyphRange = glyphRange;
li.characterRange = [tl->layoutManager characterRangeForGlyphRange:li.glyphRange actualGlyphRange:NULL];
li.lineUsedRect = [tl->layoutManager lineFragmentUsedRectForGlyphAtIndex:index effectiveRange:NULL];
li.glyphBoundingRect = [tl->layoutManager boundingRectForGlyphRange:li.glyphRange inTextContainer:tl->container];
// and this is from http://www.cocoabuilder.com/archive/cocoa/308568-how-to-get-baseline-info.html and http://www.cocoabuilder.com/archive/cocoa/199283-height-and-location-of-text-within-line-in-nslayoutmanager-ignoring-spacing.html
li.baselineOffset = [[tl->layoutManager typesetter] baselineOffsetInLayoutManager:tl->layoutManager glyphIndex:index];
li.ascent = 0;
li.descent = 0;
li.leading = 0;
// imitate what AppKit actually does (or seems to)
[tl->attrstr enumerateAttributesInRange:li.characterRange options:0 usingBlock:^(NSDictionary<NSString *,id> *attrs, NSRange range, BOOL *stop) {
NSFont *f;
NSRect boundingRect;
double v, realAscent, realDescent, realLeading;
BOOL skipAdjust, doAdjust;
f = (NSFont *) [attrs objectForKey:NSFontAttributeName];
if (f == nil) {
f = [NSFont fontWithName:@"Helvetica" size:12.0];
if (f == nil)
f = [NSFont systemFontOfSize:12.0];
}
boundingRect = [f boundingRectForFont];
realAscent = [f ascender];
realDescent = -[f descender];
realLeading = [f leading];
skipAdjust = NO;
doAdjust = NO;
if (NSMaxY(boundingRect) <= realAscent) {
// ascent entirely within bounding box
// don't do anything if there's leading; I'm guessing it's a combination of both of the reasons to skip below... (least sure of this one)
if (realLeading != 0)
skipAdjust = YES;
// does the descent slip outside the bounding box?
if (-realDescent <= NSMinY(boundingRect))
// yes — I guess we should assume accents don't collide with the previous line's descent, though I'm not as sure of that as I am about the else clause below...
skipAdjust = YES;
} else {
// ascent outside bounding box — ascent does not include accents
// only skip adjustment if there isn't leading (apparently some fonts use the previous line's leading for accents? :/ )
if (realLeading != 0)
skipAdjust = YES;
}
if (!skipAdjust) {
UniChar ch = 0xC0;
CGGlyph glyph;
// there does not seem to be an AppKit API for this...
if (CTFontGetGlyphsForCharacters((CTFontRef) f, &ch, &glyph, 1) != false) {
NSRect bbox;
bbox = [f boundingRectForGlyph:glyph];
if (NSMaxY(bbox) > realAscent)
doAdjust = YES;
if (-realDescent > NSMinY(bbox))
doAdjust = YES;
}
}
// TODO vertical
v = [f ascender];
// TODO get this one back out
if (doAdjust)
v += 0.2 * ([f ascender] + [f descender]);
//v = floor(v + 0.5);
if (li.ascent < v)
li.ascent = v;
v = -[f descender];// floor(-[f descender] + 0.5);
if (li.descent < v)
li.descent = v;
v = [f leading];//floor([f leading] + 0.5);
if (li.leading < v)
li.leading = v;
}];
li.ascent = floor(li.ascent + 0.5);
li.descent = floor(li.descent + 0.5);
li.leading = floor(li.leading + 0.5);
[tl->lineInfo addObject:li];
[li release];
index = glyphRange.location + glyphRange.length;
}
// and finally copy the UTF-16 to UTF-8 index conversion table
tl->u16tou8 = attrstrCopyUTF16ToUTF8(s, &(tl->nu16tou8));
return tl;
}
void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
{
uiFree(tl->u16tou8);
[tl->lineInfo release];
[tl->layoutManager release];
[tl->container release];
[tl->attrstr release];
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)
{
NSGraphicsContext *gc;
CGContextFlush(c->c); // just to be safe
[NSGraphicsContext saveGraphicsState];
gc = [NSGraphicsContext graphicsContextWithGraphicsPort:c->c flipped:YES];
[NSGraphicsContext setCurrentContext:gc];
// TODO is this the right point?
// TODO does this draw with the proper default styles?
[tl->layoutManager drawGlyphsForGlyphRange:[tl->layoutManager glyphRangeForTextContainer:tl->container]
atPoint:NSMakePoint(x, y)];
[gc flushGraphics]; // just to be safe
[NSGraphicsContext restoreGraphicsState];
// TODO release gc?
}
// TODO update all of these {
// 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
// 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)
{
NSRect r;
// see https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TextLayout/Tasks/StringHeight.html
r = [tl->layoutManager usedRectForTextContainer:tl->container];
*width = r.size.width;
*height = r.size.height;
}
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
{
return [tl->lineInfo count];
}
void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
{
lineInfo *li;
li = (lineInfo *) [tl->lineInfo objectAtIndex:line];
*start = tl->u16tou8[li.characterRange.location];
*end = tl->u16tou8[li.characterRange.location + li.characterRange.length];
}
void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
{
lineInfo *li;
li = (lineInfo *) [tl->lineInfo objectAtIndex:line];
m->X = li.lineRect.origin.x;
m->Y = li.lineRect.origin.y;
// if we use li.lineRect here we get the whole line, not just the part with stuff in it
m->Width = li.lineUsedRect.size.width;
m->Height = li.lineRect.size.height;
// TODO is this correct?
m->BaselineY = (m->Y + m->Height) - li.baselineOffset;
m->Ascent = li.ascent;
m->Descent = li.descent;
m->Leading = li.leading;
// TODO
m->ParagraphSpacingBefore = 0;
m->LineHeightSpace = 0;
m->LineSpacing = 0;
m->ParagraphSpacing = 0;
}
void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, uiDrawTextLayoutHitTestResult *result)
{
#if 0 /* TODO */
CFIndex i;
CTLineRef line;
CFIndex pos;
if (y >= 0) {
for (i = 0; i < tl->nLines; i++) {
double ltop, lbottom;
ltop = tl->lineMetrics[i].Y;
lbottom = ltop + tl->lineMetrics[i].Height;
if (y >= ltop && y < lbottom)
break;
}
result->YPosition = uiDrawTextLayoutHitTestPositionInside;
if (i == tl->nLines) {
i--;
result->YPosition = uiDrawTextLayoutHitTestPositionAfter;
}
} else {
i = 0;
result->YPosition = uiDrawTextLayoutHitTestPositionBefore;
}
result->Line = i;
result->XPosition = uiDrawTextLayoutHitTestPositionInside;
if (x < tl->lineMetrics[i].X) {
result->XPosition = uiDrawTextLayoutHitTestPositionBefore;
// and forcibly return the first character
x = tl->lineMetrics[i].X;
} else if (x > (tl->lineMetrics[i].X + tl->lineMetrics[i].Width)) {
result->XPosition = uiDrawTextLayoutHitTestPositionAfter;
// and forcibly return the last character
x = tl->lineMetrics[i].X + tl->lineMetrics[i].Width;
}
line = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, i);
// TODO copy the part from the docs about this point
pos = CTLineGetStringIndexForPosition(line, CGPointMake(x, 0));
if (pos == kCFNotFound) {
// TODO
}
result->Pos = tl->u16tou8[pos];
#endif
}
void uiDrawTextLayoutByteRangeToRectangle(uiDrawTextLayout *tl, size_t start, size_t end, uiDrawTextLayoutByteRangeRectangle *r)
{
}