First Commit
This commit is contained in:
commit
ae0fab9400
424 files changed
+71006
No files matched your search
@@ -0,0 +1,92 @@
|
||||
// 10 february 2017
|
||||
#include "../ui.h"
|
||||
#include "uipriv.h"
|
||||
|
||||
// TODO this doesn't handle the case where nLines == 0
|
||||
// TODO this should never happen even if there are no characters??
|
||||
// TODO figure out how to make this work on GTK+
|
||||
void uiDrawCaret(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout, size_t pos, int *line)
|
||||
{
|
||||
double xoff;
|
||||
uiDrawTextLayoutLineMetrics m;
|
||||
struct caretDrawParams cdp;
|
||||
uiDrawPath *path;
|
||||
uiDrawBrush brush;
|
||||
|
||||
if (*line < 0)
|
||||
*line = 0;
|
||||
if (*line > (uiDrawTextLayoutNumLines(layout) - 1))
|
||||
*line = (uiDrawTextLayoutNumLines(layout) - 1);
|
||||
// TODO cap pos
|
||||
xoff = uiDrawTextLayoutByteLocationInLine(layout, pos, *line);
|
||||
while (xoff < 0) {
|
||||
size_t start, end;
|
||||
|
||||
uiDrawTextLayoutLineByteRange(layout, *line, &start, &end);
|
||||
if (end < pos) // too far up
|
||||
(*line)++;
|
||||
else
|
||||
(*line)--;
|
||||
xoff = uiDrawTextLayoutByteLocationInLine(layout, pos, *line);
|
||||
}
|
||||
uiDrawTextLayoutLineGetMetrics(layout, *line, &m);
|
||||
|
||||
caretDrawParams(c, m.Height, &cdp);
|
||||
|
||||
uiDrawSave(c);
|
||||
|
||||
path = uiDrawNewPath(uiDrawFillModeWinding);
|
||||
uiDrawPathAddRectangle(path,
|
||||
// TODO add m.X?
|
||||
x + xoff - cdp.xoff, y + m.Y,
|
||||
cdp.width, m.Height);
|
||||
uiDrawPathEnd(path);
|
||||
brush.Type = uiDrawBrushTypeSolid;
|
||||
brush.R = cdp.r;
|
||||
brush.G = cdp.g;
|
||||
brush.B = cdp.b;
|
||||
brush.A = cdp.a;
|
||||
uiDrawFill(c, path, &brush);
|
||||
uiDrawFreePath(path);
|
||||
|
||||
uiDrawRestore(c);
|
||||
}
|
||||
|
||||
void drawTextBackground(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout, size_t start, size_t end, uiDrawBrush *brush, int isSelection)
|
||||
{
|
||||
int line, nLines;
|
||||
size_t lstart, lend;
|
||||
double layoutwid, layoutht;
|
||||
|
||||
uiDrawTextLayoutExtents(layout, &layoutwid, &layoutht);
|
||||
nLines = uiDrawTextLayoutNumLines(layout);
|
||||
for (line = 0; line < nLines; line++) {
|
||||
uiDrawTextLayoutLineByteRange(layout, line, &lstart, &lend);
|
||||
if (start >= lstart && start < lend)
|
||||
break;
|
||||
}
|
||||
while (end - start > 0) {
|
||||
uiDrawTextLayoutLineMetrics m;
|
||||
double startx, endx;
|
||||
uiDrawPath *path;
|
||||
|
||||
uiDrawTextLayoutLineByteRange(layout, line, &lstart, &lend);
|
||||
if (lend > end) // don't cross lines
|
||||
lend = end;
|
||||
startx = uiDrawTextLayoutByteLocationInLine(layout, start, line);
|
||||
// TODO explain this; note the use of start with lend
|
||||
endx = layoutwid;
|
||||
if (!isSelection || end <= lend)
|
||||
endx = uiDrawTextLayoutByteLocationInLine(layout, lend, line);
|
||||
uiDrawTextLayoutLineGetMetrics(layout, line, &m);
|
||||
path = uiDrawNewPath(uiDrawFillModeWinding);
|
||||
uiDrawPathAddRectangle(path,
|
||||
x + startx, y + m.Y,
|
||||
endx - startx, m.Height);
|
||||
uiDrawPathEnd(path);
|
||||
uiDrawFill(c, path, brush);
|
||||
uiDrawFreePath(path);
|
||||
line++;
|
||||
start = lend;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
// TODO split these into a separate header file?
|
||||
|
||||
// drawtext.c
|
||||
struct caretDrawParams {
|
||||
double r;
|
||||
double g;
|
||||
double b;
|
||||
double a;
|
||||
double xoff;
|
||||
double width;
|
||||
};
|
||||
extern void caretDrawParams(uiDrawContext *c, double height, struct caretDrawParams *p);
|
||||
extern void drawTextBackground(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout, size_t start, size_t end, uiDrawBrush *brush, int isSelection);
|
||||
@@ -0,0 +1,347 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// 3 january 2017
|
||||
#import "uipriv_darwin.h"
|
||||
|
||||
// Stupidity: Core Text requires an **exact match for the entire traits dictionary**, otherwise it will **drop ALL the traits**.
|
||||
// This seems to be true for every function in Core Text that advertises that it performs font matching; I can confirm at the time of writing this is the case for
|
||||
// - CTFontDescriptorCreateMatchingFontDescriptors() (though the conditions here seem odd)
|
||||
// - CTFontCreateWithFontDescriptor()
|
||||
// - CTFontCreateCopyWithAttributes()
|
||||
// And as a bonus prize, this also applies to Cocoa's NSFontDescriptor methods as well!
|
||||
// We have to implement the closest match ourselves.
|
||||
// This needs to be done early in the matching process; in particular, we have to do this before adding any features (such as small caps), because the matching descriptors won't have those.
|
||||
|
||||
struct closeness {
|
||||
NSUInteger index;
|
||||
double weight;
|
||||
double italic;
|
||||
double stretch;
|
||||
double distance;
|
||||
};
|
||||
|
||||
static double doubleAttr(NSDictionary *traits, NSString *attr)
|
||||
{
|
||||
NSNumber *n;
|
||||
|
||||
n = (NSNumber *) [traits objectForKey:attr];
|
||||
return [n doubleValue];
|
||||
}
|
||||
|
||||
struct italicCloseness {
|
||||
double normal;
|
||||
double oblique;
|
||||
double italic;
|
||||
};
|
||||
|
||||
// remember that in closeness, 0 means exact
|
||||
// in this case, since we define the range, we use 0.5 to mean "close enough" (oblique for italic and italic for oblique) and 1 to mean "not a match"
|
||||
static const struct italicCloseness italicClosenesses[] = {
|
||||
[uiDrawTextItalicNormal] = { 0, 1, 1 },
|
||||
[uiDrawTextItalicOblique] = { 1, 0, 0.5 },
|
||||
[uiDrawTextItalicItalic] = { 1, 0.5, 0 },
|
||||
};
|
||||
|
||||
// Italics are hard because Core Text does NOT distinguish between italic and oblique.
|
||||
// All Core Text provides is a slant value and the italic bit of the symbolic traits mask.
|
||||
// However, Core Text does seem to guarantee (from experimentation; see below) that the slant will be nonzero if and only if the italic bit is set, so we don't need to use the slant value.
|
||||
// Core Text also seems to guarantee that if a font lists itself as Italic or Oblique by name (font subfamily name, font style name, whatever), it will also have that bit set, so testing this bit does cover all fonts that name themselves as Italic and Oblique. (Again, this is from the below experimentation.)
|
||||
// TODO there is still one catch that might matter from a user's POV: the reverse is not true — the italic bit can be set even if the style of the font face/subfamily/style isn't named as Italic (for example, script typefaces like Adobe's Palace Script MT Std); I don't know what to do about this... I know how to start: find a script font that has an italic form (Adobe's Palace Script MT Std does not; only Regular and Semibold)
|
||||
// TODO see if the above applies to Cocoa as well
|
||||
static double italicCloseness(NSFontDescriptor *desc, NSDictionary *traits, uiDrawTextItalic italic)
|
||||
{
|
||||
const struct italicCloseness *ic = &(italicClosenesses[italic]);
|
||||
NSNumber *num;
|
||||
NSFontSymbolicTraits symbolic;
|
||||
NSString *styleName;
|
||||
NSRange range;
|
||||
BOOL isOblique;
|
||||
|
||||
num = (NSNumber *) [traits objectForKey:NSFontSymbolicTrait];
|
||||
// TODO this should really be a uint32_t-specific one
|
||||
symbolic = (NSFontSymbolicTraits) [num unsignedIntegerValue];
|
||||
if ((symbolic & NSFontItalicTrait) == 0)
|
||||
return ic->normal;
|
||||
|
||||
// Okay, now we know it's either Italic or Oblique
|
||||
// Pango's Core Text code just does a g_strrstr() (backwards case-sensitive search) for "Oblique" in the font's style name (see https://git.gnome.org/browse/pango/tree/pango/pangocoretext-fontmap.c); let's do that too I guess
|
||||
// note that NSFontFaceAttribute is the Cocoa equivalent of the style name
|
||||
isOblique = NO; // default value
|
||||
styleName = (NSString *) [desc objectForKey:NSFontFaceAttribute];
|
||||
// TODO is styleName guaranteed?
|
||||
// TODO NSLiteralSearch?
|
||||
range = [styleName rangeOfString:@"Oblique" options:NSCaseInsensitiveSearch];
|
||||
if (range.location != NSNotFound)
|
||||
return ic->oblique;
|
||||
return ic->italic;
|
||||
}
|
||||
|
||||
static NSFontDescriptor *matchTraits(NSFontDescriptor *against, double targetWeight, uiDrawTextItalic targetItalic, double targetStretch)
|
||||
{
|
||||
NSArray<NSFontDescriptor *> *matching;
|
||||
NSUInteger i, n;
|
||||
struct closeness *closeness;
|
||||
NSFontDescriptor *current;
|
||||
NSFontDescriptor *out;
|
||||
|
||||
matching = [against matchingFontDescriptorsWithMandatoryKeys:nil];
|
||||
if (matching == nil)
|
||||
// no matches; give the original back and hope for the best
|
||||
return against;
|
||||
n = [matching count];
|
||||
if (n == 0) {
|
||||
// likewise
|
||||
//TODO [matching release];
|
||||
return against;
|
||||
}
|
||||
|
||||
closeness = (struct closeness *) uiAlloc(n * sizeof (struct closeness), "struct closeness[]");
|
||||
for (i = 0; i < n; i++) {
|
||||
NSDictionary *traits;
|
||||
|
||||
closeness[i].index = i;
|
||||
current = (NSFontDescriptor *) [matching objectAtIndex:i];
|
||||
traits = (NSDictionary *) [current objectForKey:NSFontTraitsAttribute];
|
||||
if (traits == nil) {
|
||||
// couldn't get traits; be safe by ranking it lowest
|
||||
// LONGTERM figure out what the longest possible distances are
|
||||
closeness[i].weight = 3;
|
||||
closeness[i].italic = 2;
|
||||
closeness[i].stretch = 3;
|
||||
continue;
|
||||
}
|
||||
closeness[i].weight = doubleAttr(traits, NSFontWeightTrait) - targetWeight;
|
||||
closeness[i].italic = italicCloseness(current, traits, targetItalic);
|
||||
closeness[i].stretch = doubleAttr(traits, NSFontWidthTrait) - targetStretch;
|
||||
// TODO release traits?
|
||||
}
|
||||
|
||||
// now figure out the 3-space difference between the three and sort by that
|
||||
// TODO merge this loop with the previous loop?
|
||||
for (i = 0; i < n; i++) {
|
||||
double weight, italic, stretch;
|
||||
|
||||
weight = closeness[i].weight;
|
||||
weight *= weight;
|
||||
italic = closeness[i].italic;
|
||||
italic *= italic;
|
||||
stretch = closeness[i].stretch;
|
||||
stretch *= stretch;
|
||||
closeness[i].distance = sqrt(weight + italic + stretch);
|
||||
}
|
||||
qsort_b(closeness, n, sizeof (struct closeness), ^(const void *aa, const void *bb) {
|
||||
const struct closeness *a = (const struct closeness *) aa;
|
||||
const struct closeness *b = (const struct closeness *) bb;
|
||||
|
||||
// via http://www.gnu.org/software/libc/manual/html_node/Comparison-Functions.html#Comparison-Functions
|
||||
// LONGTERM is this really the best way? isn't it the same as if (*a < *b) return -1; if (*a > *b) return 1; return 0; ?
|
||||
return (a->distance > b->distance) - (a->distance < b->distance);
|
||||
});
|
||||
// and the first element of the sorted array is what we want
|
||||
out = (NSFontDescriptor *) [matching objectAtIndex:closeness[0].index];
|
||||
// TODO is this correct?
|
||||
[out retain]; // get rule
|
||||
|
||||
// release everything
|
||||
uiFree(closeness);
|
||||
//TODO [matching release];
|
||||
// and release the original descriptor since we no longer need it
|
||||
[against release];
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// since uiDrawTextWeight effectively corresponds to OS/2 weights (which roughly correspond to GDI, Pango, and DirectWrite weights, and to a lesser(? TODO) degree, CSS weights), let's just do what Core Text does with OS/2 weights
|
||||
// TODO this will not be correct for system fonts, which use cached values that have no relation to the OS/2 weights; we need to figure out how to reconcile these
|
||||
// for more information, see https://bugzilla.gnome.org/show_bug.cgi?id=766148 and TODO_put_blog_post_here_once_I_write_it (TODO keep this line when resolving the above TODO)
|
||||
static const double weightsToCTWeights[] = {
|
||||
-1.0, // 0..99
|
||||
-0.7, // 100..199
|
||||
-0.5, // 200..299
|
||||
-0.23, // 300..399
|
||||
0.0, // 400..499
|
||||
0.2, // 500..599
|
||||
0.3, // 600..699
|
||||
0.4, // 700..799
|
||||
0.6, // 800..899
|
||||
0.8, // 900..999
|
||||
1.0, // 1000
|
||||
};
|
||||
|
||||
static double weightToCTWeight(uiDrawTextWeight weight)
|
||||
{
|
||||
int weightClass;
|
||||
double ctclass;
|
||||
double rest, weightFloor, nextFloor;
|
||||
|
||||
if (weight <= 0)
|
||||
return -1.0;
|
||||
if (weight >= 1000)
|
||||
return 1.0;
|
||||
|
||||
weightClass = weight / 100;
|
||||
rest = (double) weight;
|
||||
weightFloor = (double) (weightClass * 100);
|
||||
nextFloor = (double) ((weightClass + 1) * 100);
|
||||
rest = (rest - weightFloor) / (nextFloor - weightFloor);
|
||||
|
||||
ctclass = weightsToCTWeights[weightClass];
|
||||
return fma(rest,
|
||||
weightsToCTWeights[weightClass + 1] - ctclass,
|
||||
ctclass);
|
||||
}
|
||||
|
||||
// based on what Core Text says about actual fonts (system fonts, system fonts in another folder to avoid using cached values, Adobe Font Folio 11, Google Fonts archive, fonts in Windows 7/8.1/10)
|
||||
static const double stretchesToCTWidths[] = {
|
||||
[uiDrawTextStretchUltraCondensed] = -0.400000,
|
||||
[uiDrawTextStretchExtraCondensed] = -0.300000,
|
||||
[uiDrawTextStretchCondensed] = -0.200000,
|
||||
[uiDrawTextStretchSemiCondensed] = -0.100000,
|
||||
[uiDrawTextStretchNormal] = 0.000000,
|
||||
[uiDrawTextStretchSemiExpanded] = 0.100000,
|
||||
[uiDrawTextStretchExpanded] = 0.200000,
|
||||
[uiDrawTextStretchExtraExpanded] = 0.300000,
|
||||
// this one isn't present in any of the fonts I tested, but it follows naturally from the pattern of the rest, so... (TODO verify by checking the font files directly)
|
||||
[uiDrawTextStretchUltraExpanded] = 0.400000,
|
||||
};
|
||||
|
||||
NSFontDescriptor *fontdescToNSFontDescriptor(uiDrawFontDescriptor *fd)
|
||||
{
|
||||
NSMutableDictionary *attrs;
|
||||
NSFontDescriptor *basedesc;
|
||||
|
||||
attrs = [NSMutableDictionary new];
|
||||
[attrs setObject:[NSString stringWithUTF8String:fd->Family]
|
||||
forKey:NSFontFamilyAttribute];
|
||||
[attrs setObject:[NSNumber numberWithDouble:fd->Size]
|
||||
forKey:NSFontSizeAttribute];
|
||||
|
||||
basedesc = [[NSFontDescriptor alloc] initWithFontAttributes:attrs];
|
||||
[attrs release];
|
||||
return matchTraits(basedesc,
|
||||
weightToCTWeight(fd->Weight),
|
||||
fd->Italic,
|
||||
stretchesToCTWidths[fd->Stretch]);
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// 6 september 2015
|
||||
#import "uipriv_darwin.h"
|
||||
|
||||
// TODO double-check that we are properly handling allocation failures (or just toll free bridge from cocoa)
|
||||
struct uiDrawFontFamilies {
|
||||
CFArrayRef fonts;
|
||||
};
|
||||
|
||||
uiDrawFontFamilies *uiDrawListFontFamilies(void)
|
||||
{
|
||||
uiDrawFontFamilies *ff;
|
||||
|
||||
ff = uiNew(uiDrawFontFamilies);
|
||||
ff->fonts = CTFontManagerCopyAvailableFontFamilyNames();
|
||||
if (ff->fonts == NULL)
|
||||
implbug("error getting available font names (no reason specified) (TODO)");
|
||||
return ff;
|
||||
}
|
||||
|
||||
int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff)
|
||||
{
|
||||
return CFArrayGetCount(ff->fonts);
|
||||
}
|
||||
|
||||
char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n)
|
||||
{
|
||||
CFStringRef familystr;
|
||||
char *family;
|
||||
|
||||
familystr = (CFStringRef) CFArrayGetValueAtIndex(ff->fonts, n);
|
||||
// toll-free bridge
|
||||
family = uiDarwinNSStringToText((NSString *) familystr);
|
||||
// Get Rule means we do not free familystr
|
||||
return family;
|
||||
}
|
||||
|
||||
void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff)
|
||||
{
|
||||
CFRelease(ff->fonts);
|
||||
uiFree(ff);
|
||||
}
|
||||
|
||||
struct uiDrawTextFont {
|
||||
CTFontRef f;
|
||||
};
|
||||
|
||||
uiDrawTextFont *mkTextFont(CTFontRef f, BOOL retain)
|
||||
{
|
||||
uiDrawTextFont *font;
|
||||
|
||||
font = uiNew(uiDrawTextFont);
|
||||
font->f = f;
|
||||
if (retain)
|
||||
CFRetain(font->f);
|
||||
return font;
|
||||
}
|
||||
|
||||
uiDrawTextFont *mkTextFontFromNSFont(NSFont *f)
|
||||
{
|
||||
// toll-free bridging; we do retain, though
|
||||
return mkTextFont((CTFontRef) f, YES);
|
||||
}
|
||||
|
||||
static CFMutableDictionaryRef newAttrList(void)
|
||||
{
|
||||
CFMutableDictionaryRef attr;
|
||||
|
||||
attr = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
if (attr == NULL)
|
||||
complain("error creating attribute dictionary in newAttrList()()");
|
||||
return attr;
|
||||
}
|
||||
|
||||
static void addFontFamilyAttr(CFMutableDictionaryRef attr, const char *family)
|
||||
{
|
||||
CFStringRef cfstr;
|
||||
|
||||
cfstr = CFStringCreateWithCString(NULL, family, kCFStringEncodingUTF8);
|
||||
if (cfstr == NULL)
|
||||
complain("error creating font family name CFStringRef in addFontFamilyAttr()");
|
||||
CFDictionaryAddValue(attr, kCTFontFamilyNameAttribute, cfstr);
|
||||
CFRelease(cfstr); // dictionary holds its own reference
|
||||
}
|
||||
|
||||
static void addFontSizeAttr(CFMutableDictionaryRef attr, double size)
|
||||
{
|
||||
CFNumberRef n;
|
||||
|
||||
n = CFNumberCreate(NULL, kCFNumberDoubleType, &size);
|
||||
CFDictionaryAddValue(attr, kCTFontSizeAttribute, n);
|
||||
CFRelease(n);
|
||||
}
|
||||
|
||||
#if 0
|
||||
TODO
|
||||
// See http://stackoverflow.com/questions/4810409/does-coretext-support-small-caps/4811371#4811371 and https://git.gnome.org/browse/pango/tree/pango/pangocoretext-fontmap.c for what these do
|
||||
// And fortunately, unlike the traits (see below), unmatched features are simply ignored without affecting the other features :D
|
||||
static void addFontSmallCapsAttr(CFMutableDictionaryRef attr)
|
||||
{
|
||||
CFMutableArrayRef outerArray;
|
||||
CFMutableDictionaryRef innerDict;
|
||||
CFNumberRef numType, numSelector;
|
||||
int num;
|
||||
|
||||
outerArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
||||
if (outerArray == NULL)
|
||||
complain("error creating outer CFArray for adding small caps attributes in addFontSmallCapsAttr()");
|
||||
|
||||
// Apple's headers say these are deprecated, but a few fonts still rely on them
|
||||
num = kLetterCaseType;
|
||||
numType = CFNumberCreate(NULL, kCFNumberIntType, &num);
|
||||
num = kSmallCapsSelector;
|
||||
numSelector = CFNumberCreate(NULL, kCFNumberIntType, &num);
|
||||
innerDict = newAttrList();
|
||||
CFDictionaryAddValue(innerDict, kCTFontFeatureTypeIdentifierKey, numType);
|
||||
CFRelease(numType);
|
||||
CFDictionaryAddValue(innerDict, kCTFontFeatureSelectorIdentifierKey, numSelector);
|
||||
CFRelease(numSelector);
|
||||
CFArrayAppendValue(outerArray, innerDict);
|
||||
CFRelease(innerDict); // and likewise for CFArray
|
||||
|
||||
// these are the non-deprecated versions of the above; some fonts have these instead
|
||||
num = kLowerCaseType;
|
||||
numType = CFNumberCreate(NULL, kCFNumberIntType, &num);
|
||||
num = kLowerCaseSmallCapsSelector;
|
||||
numSelector = CFNumberCreate(NULL, kCFNumberIntType, &num);
|
||||
innerDict = newAttrList();
|
||||
CFDictionaryAddValue(innerDict, kCTFontFeatureTypeIdentifierKey, numType);
|
||||
CFRelease(numType);
|
||||
CFDictionaryAddValue(innerDict, kCTFontFeatureSelectorIdentifierKey, numSelector);
|
||||
CFRelease(numSelector);
|
||||
CFArrayAppendValue(outerArray, innerDict);
|
||||
CFRelease(innerDict); // and likewise for CFArray
|
||||
|
||||
CFDictionaryAddValue(attr, kCTFontFeatureSettingsAttribute, outerArray);
|
||||
CFRelease(outerArray);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
// Named constants for these were NOT added until 10.11, and even then they were added as external symbols instead of macros, so we can't use them directly :(
|
||||
// kode54 got these for me before I had access to El Capitan; thanks to him.
|
||||
#define ourNSFontWeightUltraLight -0.800000
|
||||
#define ourNSFontWeightThin -0.600000
|
||||
#define ourNSFontWeightLight -0.400000
|
||||
#define ourNSFontWeightRegular 0.000000
|
||||
#define ourNSFontWeightMedium 0.230000
|
||||
#define ourNSFontWeightSemibold 0.300000
|
||||
#define ourNSFontWeightBold 0.400000
|
||||
#define ourNSFontWeightHeavy 0.560000
|
||||
#define ourNSFontWeightBlack 0.620000
|
||||
#endif
|
||||
|
||||
// Now remember what I said earlier about having to add the small caps traits after calling the above? This gets a dictionary back so we can do so.
|
||||
CFMutableDictionaryRef extractAttributes(CTFontDescriptorRef desc)
|
||||
{
|
||||
CFDictionaryRef dict;
|
||||
CFMutableDictionaryRef mdict;
|
||||
|
||||
dict = CTFontDescriptorCopyAttributes(desc);
|
||||
// this might not be mutable, so make a mutable copy
|
||||
mdict = CFDictionaryCreateMutableCopy(NULL, 0, dict);
|
||||
CFRelease(dict);
|
||||
return mdict;
|
||||
}
|
||||
|
||||
uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc)
|
||||
{
|
||||
CTFontRef f;
|
||||
CFMutableDictionaryRef attr;
|
||||
CTFontDescriptorRef cfdesc;
|
||||
|
||||
attr = newAttrList();
|
||||
addFontFamilyAttr(attr, desc->Family);
|
||||
addFontSizeAttr(attr, desc->Size);
|
||||
|
||||
// now we have to do the traits matching, so create a descriptor, match the traits, and then get the attributes back
|
||||
cfdesc = CTFontDescriptorCreateWithAttributes(attr);
|
||||
// TODO release attr?
|
||||
cfdesc = matchTraits(cfdesc, desc->Weight, desc->Italic, desc->Stretch);
|
||||
|
||||
// specify the initial size again just to be safe
|
||||
f = CTFontCreateWithFontDescriptor(cfdesc, desc->Size, NULL);
|
||||
// TODO release cfdesc?
|
||||
|
||||
return mkTextFont(f, NO); // we hold the initial reference; no need to retain again
|
||||
}
|
||||
|
||||
void uiDrawFreeTextFont(uiDrawTextFont *font)
|
||||
{
|
||||
CFRelease(font->f);
|
||||
uiFree(font);
|
||||
}
|
||||
|
||||
uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font)
|
||||
{
|
||||
return (uintptr_t) (font->f);
|
||||
}
|
||||
|
||||
void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
// text sizes and user space points are identical:
|
||||
// - https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TypoFeatures/TextSystemFeatures.html#//apple_ref/doc/uid/TP40009459-CH6-51627-BBCCHIFF text points are 72 per inch
|
||||
// - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Transforms/Transforms.html#//apple_ref/doc/uid/TP40003290-CH204-SW5 user space points are 72 per inch
|
||||
void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics)
|
||||
{
|
||||
metrics->Ascent = CTFontGetAscent(font->f);
|
||||
metrics->Descent = CTFontGetDescent(font->f);
|
||||
metrics->Leading = CTFontGetLeading(font->f);
|
||||
metrics->UnderlinePos = CTFontGetUnderlinePosition(font->f);
|
||||
metrics->UnderlineThickness = CTFontGetUnderlineThickness(font->f);
|
||||
}
|
||||
|
||||
// LONGTERM allow line separation and leading to be factored into a wrapping text layout
|
||||
|
||||
// TODO reconcile differences in character wrapping on platforms
|
||||
void uiDrawTextLayoutExtents(uiDrawTextLayout *layout, double *width, double *height)
|
||||
{
|
||||
struct framesetter fs;
|
||||
|
||||
mkFramesetter(layout, &fs);
|
||||
*width = fs.extents.width;
|
||||
*height = fs.extents.height;
|
||||
freeFramesetter(&fs);
|
||||
}
|
||||
|
||||
// LONGTERM provide an equivalent to CTLineGetTypographicBounds() on uiDrawTextLayout?
|
||||
|
||||
// LONGTERM keep this for later features and documentation purposes
|
||||
#if 0
|
||||
|
||||
// LONGTERM provide a way to get the image bounds as a separate function later
|
||||
bounds = CTLineGetImageBounds(line, c);
|
||||
// though CTLineGetImageBounds() returns CGRectNull on error, it also returns CGRectNull on an empty string, so we can't reasonably check for error
|
||||
|
||||
// CGContextSetTextPosition() positions at the baseline in the case of CTLineDraw(); we need the top-left corner instead
|
||||
CTLineGetTypographicBounds(line, &yoff, NULL, NULL);
|
||||
// remember that we're flipped, so we subtract
|
||||
y -= yoff;
|
||||
CGContextSetTextPosition(c, x, y);
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a)
|
||||
{
|
||||
CGColorSpaceRef colorspace;
|
||||
CGFloat components[4];
|
||||
CGColorRef color;
|
||||
|
||||
// for consistency with windows, use sRGB
|
||||
colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
components[0] = r;
|
||||
components[1] = g;
|
||||
components[2] = b;
|
||||
components[3] = a;
|
||||
color = CGColorCreate(colorspace, components);
|
||||
CGColorSpaceRelease(colorspace);
|
||||
|
||||
CFAttributedStringSetAttribute(layout->mas,
|
||||
rangeToCFRange(),
|
||||
kCTForegroundColorAttributeName,
|
||||
color);
|
||||
CGColorRelease(color); // TODO safe?
|
||||
}
|
||||
#endif
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
darwin
|
||||
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
|
||||
{
|
||||
return CFArrayGetCount([tl->forLines lines]);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
|
||||
unix
|
||||
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
|
||||
{
|
||||
return pango_layout_get_line_count(tl->layout);
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
|
||||
{
|
||||
PangoLayoutLine *pll;
|
||||
|
||||
pll = pango_layout_get_line_readonly(tl->layout, line);
|
||||
*start = pll->start_index;
|
||||
*end = pll->start_index + pll->length;
|
||||
// TODO unref pll?
|
||||
}
|
||||
|
||||
|
||||
windows
|
||||
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
|
||||
{
|
||||
return 0;
|
||||
#if 0
|
||||
TODO
|
||||
return tl->nLines;
|
||||
#endif
|
||||
}
|
||||
|
||||
// DirectWrite doesn't provide a direct way to do this, so we have to do this manually
|
||||
// TODO does that comment still apply here or to the code at the top of this file?
|
||||
void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
|
||||
{
|
||||
#if 0
|
||||
TODO
|
||||
*start = tl->lineInfo[line].startPos;
|
||||
*start = tl->u16tou8[*start];
|
||||
*end = tl->lineInfo[line].endPos - tl->lineInfo[line].newlineCount;
|
||||
*end = tl->u16tou8[*end];
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
typedef struct uiDrawTextLayoutLineMetrics uiDrawTextLayoutLineMetrics;
|
||||
|
||||
// Height will equal ParagraphSpacingBefore + LineHeightSpace + Ascent + Descent + Leading + LineSpacing + ParagraphSpacing.
|
||||
// The above values are listed in vertical order, from top to bottom.
|
||||
// Ascent + Descent + Leading will give you the typographic bounds
|
||||
// of the text. BaselineY is the boundary between Ascent and Descent.
|
||||
// X, Y, and BaselineY are all in the layout's coordinate system, so the
|
||||
// start point of the baseline will be at (X, BaselineY). All values are
|
||||
// nonnegative.
|
||||
struct uiDrawTextLayoutLineMetrics {
|
||||
// This describes the overall bounding box of the line.
|
||||
double X;
|
||||
double Y;
|
||||
double Width;
|
||||
double Height;
|
||||
|
||||
// This describes the typographic bounds of the line.
|
||||
double BaselineY;
|
||||
double Ascent;
|
||||
double Descent;
|
||||
double Leading;
|
||||
|
||||
// This describes any additional whitespace.
|
||||
// TODO come up with better names for these.
|
||||
double ParagraphSpacingBefore;
|
||||
double LineHeightSpace;
|
||||
double LineSpacing;
|
||||
double ParagraphSpacing;
|
||||
|
||||
// TODO trailing whitespace?
|
||||
};
|
||||
|
||||
// uiDrawTextLayoutNumLines() returns the number of lines in tl.
|
||||
// This number will always be greater than or equal to 1; a text
|
||||
// layout with no text only has one line.
|
||||
_UI_EXTERN int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl);
|
||||
|
||||
// uiDrawTextLayoutLineByteRange() returns the byte indices of the
|
||||
// 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);
|
||||
|
||||
_UI_EXTERN void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m);
|
||||
|
||||
// TODO rewrite this documentation
|
||||
|
||||
// uiDrawTextLayoutHitTest() returns the byte offset and line closest
|
||||
// to the given point. The point is relative to the top-left of the layout.
|
||||
// If the point is outside the layout itself, the closest point is chosen;
|
||||
// this allows the function to be used for cursor positioning with the
|
||||
// mouse. Do keep the returned line in mind if used in this way; the
|
||||
// user might click on the end of a line, at which point the cursor
|
||||
// might be at the trailing edge of the last grapheme on the line
|
||||
// (subject to the operating system's APIs).
|
||||
_UI_EXTERN void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, size_t *pos, int *line);
|
||||
|
||||
// uiDrawTextLayoutByteLocationInLine() returns the point offset
|
||||
// into the given line that the given byte position stands. This is
|
||||
// relative to the line's X position (as returned by
|
||||
// uiDrawTextLayoutLineGetMetrics()), which in turn is relative to
|
||||
// the top-left of the layout. This function can be used for cursor
|
||||
// positioning: if start and end are the start and end of the line
|
||||
// (as returned by uiDrawTextLayoutLineByteRange()), you will get
|
||||
// the correct offset, even if pos is at the end of the line. If pos is not
|
||||
// in the range [start, end], a negative value will be returned,
|
||||
// indicating you need to move the cursor to another line.
|
||||
// TODO make sure this works right for right-aligned and center-aligned lines and justified lines and RTL text
|
||||
_UI_EXTERN double uiDrawTextLayoutByteLocationInLine(uiDrawTextLayout *tl, size_t pos, int line);
|
||||
|
||||
_UI_EXTERN void uiDrawCaret(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout, size_t pos, int *line);
|
||||
// TODO allow blinking
|
||||
// TODO allow secondary carets
|
||||
@@ -0,0 +1,171 @@
|
||||
diff --git a/darwin/attrstr.m b/darwin/attrstr.m
|
||||
index fd45ec25..86039fad 100644
|
||||
--- a/darwin/attrstr.m
|
||||
+++ b/darwin/attrstr.m
|
||||
@@ -403,8 +403,13 @@ static CTParagraphStyleRef mkParagraphStyle(uiDrawTextLayoutParams *p)
|
||||
return ps;
|
||||
}
|
||||
|
||||
-CFAttributedStringRef attrstrToCoreFoundation(uiDrawTextLayoutParams *p, NSArray **backgroundBlocks)
|
||||
+static const UniChar emptyChars[] = { 0x20, 0x0 };
|
||||
+static const CFIndex emptyCharCount = 1;
|
||||
+
|
||||
+CFAttributedStringRef attrstrToCoreFoundation(uiDrawTextLayoutParams *p, BOOL *isEmpty, NSArray **backgroundBlocks)
|
||||
{
|
||||
+ const UniChar *chars;
|
||||
+ CFIndex charCount;
|
||||
CFStringRef cfstr;
|
||||
CFMutableDictionaryRef defaultAttrs;
|
||||
CTFontRef defaultCTFont;
|
||||
@@ -413,7 +418,15 @@ CFAttributedStringRef attrstrToCoreFoundation(uiDrawTextLayoutParams *p, NSArray
|
||||
CFMutableAttributedStringRef mas;
|
||||
struct foreachParams fep;
|
||||
|
||||
- cfstr = CFStringCreateWithCharacters(NULL, attrstrUTF16(p->String), attrstrUTF16Len(p->String));
|
||||
+ *isEmpty = NO;
|
||||
+ chars = attrstrUTF16(p->String);
|
||||
+ charCount = attrstrUTF16Len(p->String);
|
||||
+ if (charCount == 0) {
|
||||
+ *isEmpty = YES;
|
||||
+ chars = emptyChars;
|
||||
+ charCount = emptyCharCount;
|
||||
+ }
|
||||
+ cfstr = CFStringCreateWithCharacters(NULL, chars, charCount);
|
||||
if (cfstr == NULL) {
|
||||
// TODO
|
||||
}
|
||||
diff --git a/darwin/drawtext.m b/darwin/drawtext.m
|
||||
index 1fa5920e..65912383 100644
|
||||
--- a/darwin/drawtext.m
|
||||
+++ b/darwin/drawtext.m
|
||||
@@ -2,13 +2,16 @@
|
||||
#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 in general, every function could be more robust
|
||||
// TODO what happens to extents if only whitespace?
|
||||
+// TODO for empty layouts:
|
||||
+// - check if alignment correct compared to other OSs, or expected behavior at all
|
||||
+// - double-check if uiAttributedString allows zero-length attributes; I forget if I did
|
||||
|
||||
struct uiDrawTextLayout {
|
||||
CFAttributedStringRef attrstr;
|
||||
+ // this is needed because Core Text will give us an empty line array on a frame made with an empty string
|
||||
+ BOOL isEmpty;
|
||||
|
||||
// the width as passed into uiDrawTextLayout constructors
|
||||
double width;
|
||||
@@ -41,7 +44,7 @@
|
||||
};
|
||||
|
||||
// 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)
|
||||
+static uiDrawTextLayoutLineMetrics *computeLineMetrics(CTFrameRef frame, CGSize size, BOOL isEmpty)
|
||||
{
|
||||
uiDrawTextLayoutLineMetrics *metrics;
|
||||
CFArrayRef lines;
|
||||
@@ -79,6 +82,8 @@
|
||||
metrics[i].X = origins[i].x;
|
||||
metrics[i].Y = origins[i].y - descent - leading;
|
||||
metrics[i].Width = bounds.size.width;
|
||||
+ if (isEmpty)
|
||||
+ metrics[i].Width = 0;
|
||||
metrics[i].Height = ascent + descent + leading;
|
||||
|
||||
metrics[i].BaselineY = origins[i].y;
|
||||
@@ -117,7 +122,7 @@
|
||||
CGRect rect;
|
||||
|
||||
tl = uiNew(uiDrawTextLayout);
|
||||
- tl->attrstr = attrstrToCoreFoundation(p, &(tl->backgroundBlocks));
|
||||
+ tl->attrstr = attrstrToCoreFoundation(p, &(tl->isEmpty), &(tl->backgroundBlocks));
|
||||
range.location = 0;
|
||||
range.length = CFAttributedStringGetLength(tl->attrstr);
|
||||
tl->width = p->Width;
|
||||
@@ -152,7 +157,7 @@
|
||||
|
||||
tl->lines = CTFrameGetLines(tl->frame);
|
||||
tl->nLines = CFArrayGetCount(tl->lines);
|
||||
- tl->lineMetrics = computeLineMetrics(tl->frame, tl->size);
|
||||
+ tl->lineMetrics = computeLineMetrics(tl->frame, tl->size, tl->isEmpty);
|
||||
|
||||
// and finally copy the UTF-8/UTF-16 conversion tables
|
||||
tl->u8tou16 = attrstrCopyUTF8ToUTF16(p->String, &(tl->nUTF8));
|
||||
@@ -180,6 +185,9 @@ void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
|
||||
backgroundBlock b;
|
||||
CGAffineTransform textMatrix;
|
||||
|
||||
+ if (tl->isEmpty)
|
||||
+ return;
|
||||
+
|
||||
CGContextSaveGState(c->c);
|
||||
// save the text matrix because it's not part of the graphics state
|
||||
textMatrix = CGContextGetTextMatrix(c->c);
|
||||
@@ -216,6 +224,8 @@ void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
|
||||
void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height)
|
||||
{
|
||||
*width = tl->size.width;
|
||||
+ if (tl->isEmpty)
|
||||
+ *width = 0;
|
||||
*height = tl->size.height;
|
||||
}
|
||||
|
||||
@@ -233,6 +243,8 @@ void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start
|
||||
range = CTLineGetStringRange(lr);
|
||||
*start = tl->u16tou8[range.location];
|
||||
*end = tl->u16tou8[range.location + range.length];
|
||||
+ if (tl->isEmpty)
|
||||
+ *start = *end;
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
|
||||
@@ -262,14 +274,17 @@ void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, size_t *p
|
||||
*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 = 0;
|
||||
+ if (!tl->isEmpty) {
|
||||
+ // 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
|
||||
+ p = CTLineGetStringIndexForPosition(ln, CGPointMake(x, 0));
|
||||
+ if (p == kCFNotFound) {
|
||||
+ // TODO
|
||||
+ }
|
||||
+ *pos = tl->u16tou8[p];
|
||||
}
|
||||
- *pos = tl->u16tou8[p];
|
||||
}
|
||||
|
||||
double uiDrawTextLayoutByteLocationInLine(uiDrawTextLayout *tl, size_t pos, int line)
|
||||
@@ -282,6 +297,9 @@ void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, size_t *p
|
||||
return -1;
|
||||
lr = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, line);
|
||||
range = CTLineGetStringRange(lr);
|
||||
+ // TODO is the behavior of this part correct?
|
||||
+ if (tl->isEmpty)
|
||||
+ range.length = 0;
|
||||
// note: >, not >=, because the position at end is valid!
|
||||
if (pos < range.location || pos > (range.location + range.length))
|
||||
return -1;
|
||||
diff --git a/darwin/uipriv_darwin.h b/darwin/uipriv_darwin.h
|
||||
index 0303c32c..d44ee410 100644
|
||||
--- a/darwin/uipriv_darwin.h
|
||||
+++ b/darwin/uipriv_darwin.h
|
||||
@@ -151,7 +151,7 @@ extern void fontdescFromCTFontDescriptor(CTFontDescriptorRef ctdesc, uiDrawFontD
|
||||
extern void initUnderlineColors(void);
|
||||
extern void uninitUnderlineColors(void);
|
||||
typedef void (^backgroundBlock)(uiDrawContext *c, uiDrawTextLayout *layout, double x, double y);
|
||||
-extern CFAttributedStringRef attrstrToCoreFoundation(uiDrawTextLayoutParams *p, NSArray **backgroundBlocks);
|
||||
+extern CFAttributedStringRef attrstrToCoreFoundation(uiDrawTextLayoutParams *p, BOOL *isEmpty, NSArray **backgroundBlocks);
|
||||
|
||||
// aat.m
|
||||
typedef void (^aatBlock)(uint16_t type, uint16_t selector);
|
||||
@@ -0,0 +1,217 @@
|
||||
// 6 september 2015
|
||||
#include "uipriv_unix.h"
|
||||
#include "draw.h"
|
||||
|
||||
struct uiDrawFontFamilies {
|
||||
PangoFontFamily **f;
|
||||
int n;
|
||||
};
|
||||
|
||||
uiDrawFontFamilies *uiDrawListFontFamilies(void)
|
||||
{
|
||||
uiDrawFontFamilies *ff;
|
||||
PangoFontMap *map;
|
||||
|
||||
ff = uiNew(uiDrawFontFamilies);
|
||||
map = pango_cairo_font_map_get_default();
|
||||
pango_font_map_list_families(map, &(ff->f), &(ff->n));
|
||||
// do not free map; it's a shared resource
|
||||
return ff;
|
||||
}
|
||||
|
||||
int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff)
|
||||
{
|
||||
return ff->n;
|
||||
}
|
||||
|
||||
char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n)
|
||||
{
|
||||
PangoFontFamily *f;
|
||||
|
||||
f = ff->f[n];
|
||||
return uiUnixStrdupText(pango_font_family_get_name(f));
|
||||
}
|
||||
|
||||
void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff)
|
||||
{
|
||||
g_free(ff->f);
|
||||
uiFree(ff);
|
||||
}
|
||||
|
||||
struct uiDrawTextFont {
|
||||
PangoFont *f;
|
||||
};
|
||||
|
||||
uiDrawTextFont *mkTextFont(PangoFont *f, gboolean ref)
|
||||
{
|
||||
uiDrawTextFont *font;
|
||||
|
||||
font = uiNew(uiDrawTextFont);
|
||||
font->f = f;
|
||||
if (ref)
|
||||
g_object_ref(font->f);
|
||||
return font;
|
||||
}
|
||||
|
||||
|
||||
|
||||
PangoFont *pangoDescToPangoFont(PangoFontDescription *pdesc)
|
||||
{
|
||||
PangoFont *f;
|
||||
PangoContext *context;
|
||||
|
||||
// in this case, the context is necessary for the metrics to be correct
|
||||
context = mkGenericPangoCairoContext();
|
||||
f = pango_font_map_load_font(pango_cairo_font_map_get_default(), context, pdesc);
|
||||
if (f == NULL) {
|
||||
// LONGTERM
|
||||
g_error("[libui] no match in pangoDescToPangoFont(); report to andlabs");
|
||||
}
|
||||
g_object_unref(context);
|
||||
return f;
|
||||
}
|
||||
|
||||
uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc)
|
||||
{
|
||||
PangoFont *f;
|
||||
PangoFontDescription *pdesc;
|
||||
|
||||
pdesc = pango_font_description_new();
|
||||
pango_font_description_set_family(pdesc,
|
||||
desc->Family);
|
||||
pango_font_description_set_size(pdesc,
|
||||
(gint) (desc->Size * PANGO_SCALE));
|
||||
pango_font_description_set_weight(pdesc,
|
||||
pangoWeights[desc->Weight]);
|
||||
pango_font_description_set_style(pdesc,
|
||||
pangoItalics[desc->Italic]);
|
||||
pango_font_description_set_stretch(pdesc,
|
||||
pangoStretches[desc->Stretch]);
|
||||
f = pangoDescToPangoFont(pdesc);
|
||||
pango_font_description_free(pdesc);
|
||||
return mkTextFont(f, FALSE); // we hold the initial reference; no need to ref
|
||||
}
|
||||
|
||||
void uiDrawFreeTextFont(uiDrawTextFont *font)
|
||||
{
|
||||
g_object_unref(font->f);
|
||||
uiFree(font);
|
||||
}
|
||||
|
||||
uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font)
|
||||
{
|
||||
return (uintptr_t) (font->f);
|
||||
}
|
||||
|
||||
void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc)
|
||||
{
|
||||
PangoFontDescription *pdesc;
|
||||
|
||||
// this creates a copy; we free it later
|
||||
pdesc = pango_font_describe(font->f);
|
||||
|
||||
// TODO
|
||||
|
||||
pango_font_description_free(pdesc);
|
||||
}
|
||||
|
||||
// See https://developer.gnome.org/pango/1.30/pango-Cairo-Rendering.html#pango-Cairo-Rendering.description
|
||||
// Note that we convert to double before dividing to make sure the floating-point stuff is right
|
||||
#define pangoToCairo(pango) (((double) (pango)) / PANGO_SCALE)
|
||||
#define cairoToPango(cairo) ((gint) ((cairo) * PANGO_SCALE))
|
||||
|
||||
void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics)
|
||||
{
|
||||
PangoFontMetrics *pm;
|
||||
|
||||
pm = pango_font_get_metrics(font->f, NULL);
|
||||
metrics->Ascent = pangoToCairo(pango_font_metrics_get_ascent(pm));
|
||||
metrics->Descent = pangoToCairo(pango_font_metrics_get_descent(pm));
|
||||
// Pango doesn't seem to expose this :( Use 0 and hope for the best.
|
||||
metrics->Leading = 0;
|
||||
metrics->UnderlinePos = pangoToCairo(pango_font_metrics_get_underline_position(pm));
|
||||
metrics->UnderlineThickness = pangoToCairo(pango_font_metrics_get_underline_thickness(pm));
|
||||
pango_font_metrics_unref(pm);
|
||||
}
|
||||
|
||||
// note: PangoCairoLayouts are tied to a given cairo_t, so we can't store one in this device-independent structure
|
||||
struct uiDrawTextLayout {
|
||||
char *s;
|
||||
ptrdiff_t *graphemes;
|
||||
PangoFont *defaultFont;
|
||||
double width;
|
||||
PangoAttrList *attrs;
|
||||
};
|
||||
|
||||
uiDrawTextLayout *uiDrawNewTextLayout(const char *text, uiDrawTextFont *defaultFont, double width)
|
||||
{
|
||||
uiDrawTextLayout *layout;
|
||||
PangoContext *context;
|
||||
|
||||
layout = uiNew(uiDrawTextLayout);
|
||||
layout->s = g_strdup(text);
|
||||
context = mkGenericPangoCairoContext();
|
||||
layout->graphemes = graphemes(layout->s, context);
|
||||
g_object_unref(context);
|
||||
layout->defaultFont = defaultFont->f;
|
||||
g_object_ref(layout->defaultFont); // retain a copy
|
||||
uiDrawTextLayoutSetWidth(layout, width);
|
||||
layout->attrs = pango_attr_list_new();
|
||||
return layout;
|
||||
}
|
||||
|
||||
void uiDrawFreeTextLayout(uiDrawTextLayout *layout)
|
||||
{
|
||||
pango_attr_list_unref(layout->attrs);
|
||||
g_object_unref(layout->defaultFont);
|
||||
uiFree(layout->graphemes);
|
||||
g_free(layout->s);
|
||||
uiFree(layout);
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutSetWidth(uiDrawTextLayout *layout, double width)
|
||||
{
|
||||
layout->width = width;
|
||||
}
|
||||
|
||||
static void prepareLayout(uiDrawTextLayout *layout, PangoLayout *pl)
|
||||
{
|
||||
// again, this makes a copy
|
||||
desc = pango_font_describe(layout->defaultFont);
|
||||
|
||||
pango_layout_set_attributes(pl, layout->attrs);
|
||||
}
|
||||
|
||||
void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout)
|
||||
{
|
||||
PangoLayout *pl;
|
||||
|
||||
pl = pango_cairo_create_layout(c->cr);
|
||||
}
|
||||
|
||||
static void addAttr(uiDrawTextLayout *layout, PangoAttribute *attr, int startChar, int endChar)
|
||||
{
|
||||
attr->start_index = layout->graphemes[startChar];
|
||||
attr->end_index = layout->graphemes[endChar];
|
||||
pango_attr_list_insert(layout->attrs, attr);
|
||||
// pango_attr_list_insert() takes attr; we don't free it
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a)
|
||||
{
|
||||
PangoAttribute *attr;
|
||||
guint16 rr, gg, bb, aa;
|
||||
|
||||
rr = (guint16) (r * 65535);
|
||||
gg = (guint16) (g * 65535);
|
||||
bb = (guint16) (b * 65535);
|
||||
aa = (guint16) (a * 65535);
|
||||
|
||||
attr = pango_attr_foreground_new(rr, gg, bb);
|
||||
addAttr(layout, attr, startChar, endChar);
|
||||
|
||||
// TODO what if aa == 0?
|
||||
attr = FUTURE_pango_attr_foreground_alpha_new(aa);
|
||||
if (attr != NULL)
|
||||
addAttr(layout, attr, startChar, endChar);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// 17 january 2017
|
||||
#include "uipriv_unix.h"
|
||||
#include "draw.h"
|
||||
|
||||
// TODO
|
||||
// - if the RTL override is at the beginning of a line, the preceding space is included?
|
||||
// - nLines == 0: mostly works, except the width is wrong if the paragraph alignment is center or right...
|
||||
// - TODO check whitespace and line bounds
|
||||
|
||||
struct uiDrawTextLayout {
|
||||
PangoLayout *layout;
|
||||
GPtrArray *backgroundClosures;
|
||||
uiDrawTextLayoutLineMetrics *lineMetrics;
|
||||
// TODO change everything to use this
|
||||
int nLines;
|
||||
};
|
||||
|
||||
// TODO neither these nor the overall extents seem to include trailing whitespace... we need to figure that out too
|
||||
static void computeLineMetrics(uiDrawTextLayout *tl)
|
||||
{
|
||||
PangoLayoutIter *iter;
|
||||
PangoLayoutLine *pll;
|
||||
PangoRectangle lineStartPos, lineExtents;
|
||||
int i, n;
|
||||
uiDrawTextLayoutLineMetrics *m;
|
||||
|
||||
n = tl->nLines; // TODO remove this variable
|
||||
tl->lineMetrics = (uiDrawTextLayoutLineMetrics *) uiAlloc(n * sizeof (uiDrawTextLayoutLineMetrics), "uiDrawTextLayoutLineMetrics[] (text layout)");
|
||||
iter = pango_layout_get_iter(tl->layout);
|
||||
|
||||
m = tl->lineMetrics;
|
||||
for (i = 0; i < n; i++) {
|
||||
int baselineY;
|
||||
|
||||
// TODO we use this instead of _get_yrange() because of the block of text in that function's description about how line spacing is distributed in Pango; we have to worry about this when we start adding line spacing...
|
||||
baselineY = pango_layout_iter_get_baseline(iter);
|
||||
pll = pango_layout_iter_get_line_readonly(iter);
|
||||
pango_layout_index_to_pos(tl->layout, pll->start_index, &lineStartPos);
|
||||
pango_layout_line_get_extents(pll, NULL, &lineExtents);
|
||||
// TODO unref pll?
|
||||
|
||||
// TODO is this correct for RTL glyphs?
|
||||
m->X = pangoToCairo(lineStartPos.x);
|
||||
// TODO fix the whole combined not being updated shenanigans in the static build (here because ugh)
|
||||
m->Y = pangoToCairo(baselineY - PANGO_ASCENT(lineExtents));
|
||||
// TODO this does not include the last space if any
|
||||
m->Width = pangoToCairo(lineExtents.width);
|
||||
m->Height = pangoToCairo(lineExtents.height);
|
||||
|
||||
m->BaselineY = pangoToCairo(baselineY);
|
||||
m->Ascent = pangoToCairo(PANGO_ASCENT(lineExtents));
|
||||
m->Descent = pangoToCairo(PANGO_DESCENT(lineExtents));
|
||||
m->Leading = 0; // TODO
|
||||
|
||||
m->ParagraphSpacingBefore = 0; // TODO
|
||||
m->LineHeightSpace = 0; // TODO
|
||||
m->LineSpacing = 0; // TODO
|
||||
m->ParagraphSpacing = 0; // TODO
|
||||
|
||||
// don't worry about the return value; we're not using this after the last line
|
||||
pango_layout_iter_next_line(iter);
|
||||
m++;
|
||||
}
|
||||
|
||||
pango_layout_iter_free(iter);
|
||||
}
|
||||
|
||||
uiDrawTextLayout *uiDrawNewTextLayout(uiDrawTextLayoutParams *p)
|
||||
{
|
||||
uiDrawTextLayout *tl;
|
||||
PangoContext *context;
|
||||
PangoFontDescription *desc;
|
||||
PangoAttrList *attrs;
|
||||
int pangoWidth;
|
||||
|
||||
tl = uiNew(uiDrawTextLayout);
|
||||
|
||||
// in this case, the context is necessary to create the layout
|
||||
// the layout takes a ref on the context so we can unref it afterward
|
||||
context = mkGenericPangoCairoContext();
|
||||
tl->layout = pango_layout_new(context);
|
||||
g_object_unref(context);
|
||||
|
||||
// this is safe; pango_layout_set_text() copies the string
|
||||
pango_layout_set_text(tl->layout, uiAttributedStringString(p->String), -1);
|
||||
|
||||
desc = pango_font_description_new();
|
||||
pango_font_description_set_family(desc, p->DefaultFont->Family);
|
||||
pango_font_description_set_style(desc, pangoItalics[p->DefaultFont->Italic]);
|
||||
// for the most part, pango weights correlate to ours
|
||||
// the differences:
|
||||
// - Book — libui: 350, Pango: 380
|
||||
// - Ultra Heavy — libui: 950, Pango: 1000
|
||||
// TODO figure out what to do about this misalignment
|
||||
pango_font_description_set_weight(desc, p->DefaultFont->Weight);
|
||||
pango_font_description_set_stretch(desc, pangoStretches[p->DefaultFont->Stretch]);
|
||||
// see https://developer.gnome.org/pango/1.30/pango-Fonts.html#pango-font-description-set-size and https://developer.gnome.org/pango/1.30/pango-Glyph-Storage.html#pango-units-from-double
|
||||
pango_font_description_set_size(desc, pango_units_from_double(p->DefaultFont->Size));
|
||||
pango_layout_set_font_description(tl->layout, desc);
|
||||
// this is safe; the description is copied
|
||||
pango_font_description_free(desc);
|
||||
|
||||
pangoWidth = cairoToPango(p->Width);
|
||||
if (p->Width < 0)
|
||||
pangoWidth = -1;
|
||||
pango_layout_set_width(tl->layout, pangoWidth);
|
||||
|
||||
pango_layout_set_alignment(tl->layout, pangoAligns[p->Align]);
|
||||
|
||||
attrs = attrstrToPangoAttrList(p, &(tl->backgroundClosures));
|
||||
pango_layout_set_attributes(tl->layout, attrs);
|
||||
pango_attr_list_unref(attrs);
|
||||
|
||||
tl->nLines = pango_layout_get_line_count(tl->layout);
|
||||
computeLineMetrics(tl);
|
||||
|
||||
return tl;
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
|
||||
{
|
||||
*m = tl->lineMetrics[line];
|
||||
}
|
||||
|
||||
// TODO
|
||||
#if 0
|
||||
{
|
||||
PangoLayoutLine *pll;
|
||||
|
||||
pll = pango_layout_get_line_readonly(tl->layout, line);
|
||||
// TODO unref?
|
||||
}
|
||||
#endif
|
||||
|
||||
// note: Pango will not let us place the cursor at the end of a line the same way other OSs do; see https://git.gnome.org/browse/pango/tree/pango/pango-layout.c?id=f4cbd27f4e5bf8490ea411190d41813e14f12165#n4204
|
||||
// ideally there'd be a way to say "I don't need this hack; I'm well behaved" but GTK+ 2 and 3 AND Qt 4 and 5 all behave like this, with the behavior seeming to date back to TkTextView, so...
|
||||
void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, size_t *pos, int *line)
|
||||
{
|
||||
int p, trailing;
|
||||
int i;
|
||||
|
||||
// this is layout-global, so it takes line origins into account
|
||||
pango_layout_xy_to_index(tl->layout,
|
||||
cairoToPango(x), cairoToPango(y),
|
||||
&p, &trailing);
|
||||
// on a trailing hit, align to the nearest cluster
|
||||
// fortunately Pango provides that info directly
|
||||
if (trailing != 0)
|
||||
p += trailing;
|
||||
*pos = 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 == ltop
|
||||
if (y < lbottom)
|
||||
break;
|
||||
}
|
||||
if (i == pango_layout_get_line_count(tl->layout))
|
||||
i--;
|
||||
*line = i;
|
||||
}
|
||||
|
||||
double uiDrawTextLayoutByteLocationInLine(uiDrawTextLayout *tl, size_t pos, int line)
|
||||
{
|
||||
PangoLayoutLine *pll;
|
||||
gboolean trailing;
|
||||
int pangox;
|
||||
|
||||
if (line < 0 || line >= tl->nLines)
|
||||
return -1;
|
||||
pll = pango_layout_get_line_readonly(tl->layout, line);
|
||||
// note: >, not >=, because the position at end is valid!
|
||||
if (pos < pll->start_index || pos > (pll->start_index + pll->length))
|
||||
return -1;
|
||||
// this behavior seems correct
|
||||
// there's also PadWrite's TextEditor::GetCaretRect() but that requires state...
|
||||
// TODO where does this fail?
|
||||
// TODO optimize everything to avoid calling strlen()
|
||||
trailing = 0;
|
||||
if (pos != 0 && pos != strlen(pango_layout_get_text(tl->layout)) && pos == (pll->start_index + pll->length)) {
|
||||
pos--;
|
||||
trailing = 1;
|
||||
}
|
||||
pango_layout_line_index_to_x(pll, pos, trailing, &pangox);
|
||||
// TODO unref pll?
|
||||
// this is relative to the beginning of the line
|
||||
return pangoToCairo(pangox) + tl->lineMetrics[line].X;
|
||||
}
|
||||
|
||||
// note: we can't use gtk_render_insertion_cursor() because that doesn't take information about what line to render on
|
||||
// we'll just recreate what it does
|
||||
void caretDrawParams(uiDrawContext *c, double height, struct caretDrawParams *p)
|
||||
{
|
||||
GdkColor *color;
|
||||
GdkRGBA rgba;
|
||||
gfloat aspectRatio;
|
||||
gint width, xoff;
|
||||
|
||||
gtk_style_context_get_style(c->style,
|
||||
"cursor-color", &color,
|
||||
"cursor-aspect-ratio", &aspectRatio,
|
||||
NULL);
|
||||
if (color != NULL) {
|
||||
p->r = ((double) (color->red)) / 65535.0;
|
||||
p->g = ((double) (color->green)) / 65535.0;
|
||||
p->b = ((double) (color->blue)) / 65535.0;
|
||||
p->a = 1.0;
|
||||
gdk_color_free(color);
|
||||
} else {
|
||||
gtk_style_context_get_color(c->style, GTK_STATE_FLAG_NORMAL, &rgba);
|
||||
p->r = rgba.red;
|
||||
p->g = rgba.green;
|
||||
p->b = rgba.blue;
|
||||
p->a = rgba.alpha;
|
||||
}
|
||||
|
||||
// GTK+ itself uses integer arithmetic here; let's do the same
|
||||
width = height * aspectRatio + 1;
|
||||
// TODO this is for LTR
|
||||
xoff = width / 2;
|
||||
|
||||
p->xoff = xoff;
|
||||
p->width = width;
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// 22 december 2015
|
||||
#include "uipriv_windows.hpp"
|
||||
#include "draw.hpp"
|
||||
// TODO really migrate
|
||||
|
||||
// notes:
|
||||
// only available in windows 8 and newer:
|
||||
// - character spacing
|
||||
// - kerning control
|
||||
// - justficiation (how could I possibly be making this up?!)
|
||||
// - vertical text (SERIOUSLY?! WHAT THE ACTUAL FUCK, MICROSOFT?!?!?!? DID YOU NOT THINK ABOUT THIS THE FIRST TIME, TRYING TO IMPROVE THE INTERNATIONALIZATION OF WINDOWS 7?!?!?! bonus: some parts of MSDN even say 8.1 and up only!)
|
||||
|
||||
struct uiDrawFontFamilies {
|
||||
fontCollection *fc;
|
||||
};
|
||||
|
||||
uiDrawFontFamilies *uiDrawListFontFamilies(void)
|
||||
{
|
||||
struct uiDrawFontFamilies *ff;
|
||||
|
||||
ff = uiNew(struct uiDrawFontFamilies);
|
||||
ff->fc = loadFontCollection();
|
||||
return ff;
|
||||
}
|
||||
|
||||
int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff)
|
||||
{
|
||||
return ff->fc->fonts->GetFontFamilyCount();
|
||||
}
|
||||
|
||||
char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n)
|
||||
{
|
||||
IDWriteFontFamily *family;
|
||||
WCHAR *wname;
|
||||
char *name;
|
||||
HRESULT hr;
|
||||
|
||||
hr = ff->fc->fonts->GetFontFamily(n, &family);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error getting font out of collection", hr);
|
||||
wname = fontCollectionFamilyName(ff->fc, family);
|
||||
name = toUTF8(wname);
|
||||
uiFree(wname);
|
||||
family->Release();
|
||||
return name;
|
||||
}
|
||||
|
||||
void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff)
|
||||
{
|
||||
fontCollectionFree(ff->fc);
|
||||
uiFree(ff);
|
||||
}
|
||||
|
||||
struct uiDrawTextFont {
|
||||
IDWriteFont *f;
|
||||
WCHAR *family; // save for convenience in uiDrawNewTextLayout()
|
||||
double size;
|
||||
};
|
||||
|
||||
uiDrawTextFont *mkTextFont(IDWriteFont *df, BOOL addRef, WCHAR *family, BOOL copyFamily, double size)
|
||||
{
|
||||
uiDrawTextFont *font;
|
||||
WCHAR *copy;
|
||||
HRESULT hr;
|
||||
|
||||
font = uiNew(uiDrawTextFont);
|
||||
font->f = df;
|
||||
if (addRef)
|
||||
font->f->AddRef();
|
||||
if (copyFamily) {
|
||||
copy = (WCHAR *) uiAlloc((wcslen(family) + 1) * sizeof (WCHAR), "WCHAR[]");
|
||||
wcscpy(copy, family);
|
||||
font->family = copy;
|
||||
} else
|
||||
font->family = family;
|
||||
font->size = size;
|
||||
return font;
|
||||
}
|
||||
|
||||
// TODO MinGW-w64 is missing this one
|
||||
#define DWRITE_FONT_WEIGHT_SEMI_LIGHT (DWRITE_FONT_WEIGHT(350))
|
||||
|
||||
uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc)
|
||||
{
|
||||
uiDrawTextFont *font;
|
||||
IDWriteFontCollection *collection;
|
||||
UINT32 index;
|
||||
BOOL exists;
|
||||
struct dwriteAttr attr;
|
||||
IDWriteFontFamily *family;
|
||||
WCHAR *wfamily;
|
||||
IDWriteFont *match;
|
||||
HRESULT hr;
|
||||
|
||||
// always get the latest available font information
|
||||
hr = dwfactory->GetSystemFontCollection(&collection, TRUE);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error getting system font collection", hr);
|
||||
|
||||
wfamily = toUTF16(desc->Family);
|
||||
hr = collection->FindFamilyName(wfamily, &index, &exists);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error finding font family", hr);
|
||||
if (!exists)
|
||||
implbug("LONGTERM family not found in uiDrawLoadClosestFont()", hr);
|
||||
hr = collection->GetFontFamily(index, &family);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error loading font family", hr);
|
||||
|
||||
attr.weight = desc->Weight;
|
||||
attr.italic = desc->Italic;
|
||||
attr.stretch = desc->Stretch;
|
||||
attrToDWriteAttr(&attr);
|
||||
hr = family->GetFirstMatchingFont(
|
||||
attr.dweight,
|
||||
attr.dstretch,
|
||||
attr.ditalic,
|
||||
&match);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error loading font", hr);
|
||||
|
||||
font = mkTextFont(match,
|
||||
FALSE, // we own the initial reference; no need to add another one
|
||||
wfamily, FALSE, // will be freed with font
|
||||
desc->Size);
|
||||
|
||||
family->Release();
|
||||
collection->Release();
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
void uiDrawFreeTextFont(uiDrawTextFont *font)
|
||||
{
|
||||
font->f->Release();
|
||||
uiFree(font->family);
|
||||
uiFree(font);
|
||||
}
|
||||
|
||||
uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font)
|
||||
{
|
||||
return (uintptr_t) (font->f);
|
||||
}
|
||||
|
||||
void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc)
|
||||
{
|
||||
// TODO
|
||||
|
||||
desc->Size = font->size;
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
// text sizes are 1/72 of an inch
|
||||
// points in Direct2D are 1/96 of an inch (https://msdn.microsoft.com/en-us/library/windows/desktop/ff684173%28v=vs.85%29.aspx, https://msdn.microsoft.com/en-us/library/windows/desktop/hh447022%28v=vs.85%29.aspx)
|
||||
// As for the actual conversion from design units, see:
|
||||
// - http://cboard.cprogramming.com/windows-programming/136733-directwrite-font-height-issues.html
|
||||
// - https://sourceforge.net/p/vstgui/mailman/message/32483143/
|
||||
// - http://xboxforums.create.msdn.com/forums/t/109445.aspx
|
||||
// - https://msdn.microsoft.com/en-us/library/dd183564%28v=vs.85%29.aspx
|
||||
// - http://www.fontbureau.com/blog/the-em/
|
||||
// TODO make points here about how DIPs in DirectWrite == DIPs in Direct2D; if not, figure out what they really are? for the width and layout functions later
|
||||
static double scaleUnits(double what, double designUnitsPerEm, double size)
|
||||
{
|
||||
return (what / designUnitsPerEm) * (size * (96.0 / 72.0));
|
||||
}
|
||||
|
||||
void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics)
|
||||
{
|
||||
DWRITE_FONT_METRICS dm;
|
||||
|
||||
font->f->GetMetrics(&dm);
|
||||
metrics->Ascent = scaleUnits(dm.ascent, dm.designUnitsPerEm, font->size);
|
||||
metrics->Descent = scaleUnits(dm.descent, dm.designUnitsPerEm, font->size);
|
||||
// TODO what happens if dm.xxx is negative?
|
||||
// TODO remember what this was for
|
||||
metrics->Leading = scaleUnits(dm.lineGap, dm.designUnitsPerEm, font->size);
|
||||
metrics->UnderlinePos = scaleUnits(dm.underlinePosition, dm.designUnitsPerEm, font->size);
|
||||
metrics->UnderlineThickness = scaleUnits(dm.underlineThickness, dm.designUnitsPerEm, font->size);
|
||||
}
|
||||
|
||||
// some attributes, such as foreground color, can't be applied until after we establish a Direct2D context :/ so we have to prepare all attributes in advance
|
||||
// also since there's no way to clear the attributes from a layout en masse (apart from overwriting them all), we'll play it safe by creating a new layout each time
|
||||
enum layoutAttrType {
|
||||
layoutAttrColor,
|
||||
};
|
||||
|
||||
struct layoutAttr {
|
||||
enum layoutAttrType type;
|
||||
int start;
|
||||
int end;
|
||||
double components[4];
|
||||
};
|
||||
|
||||
struct uiDrawTextLayout {
|
||||
WCHAR *text;
|
||||
size_t textlen;
|
||||
size_t *graphemes;
|
||||
double width;
|
||||
IDWriteTextFormat *format;
|
||||
std::vector<struct layoutAttr> *attrs;
|
||||
};
|
||||
|
||||
uiDrawTextLayout *uiDrawNewTextLayout(const char *text, uiDrawTextFont *defaultFont, double width)
|
||||
{
|
||||
uiDrawTextLayout *layout;
|
||||
HRESULT hr;
|
||||
|
||||
layout = uiNew(uiDrawTextLayout);
|
||||
|
||||
hr = dwfactory->CreateTextFormat(defaultFont->family,
|
||||
NULL,
|
||||
defaultFont->f->GetWeight(),
|
||||
defaultFont->f->GetStyle(),
|
||||
defaultFont->f->GetStretch(),
|
||||
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error creating IDWriteTextFormat", hr);
|
||||
|
||||
layout->text = toUTF16(text);
|
||||
layout->textlen = wcslen(layout->text);
|
||||
layout->graphemes = graphemes(layout->text);
|
||||
|
||||
uiDrawTextLayoutSetWidth(layout, width);
|
||||
|
||||
layout->attrs = new std::vector<struct layoutAttr>;
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
void uiDrawFreeTextLayout(uiDrawTextLayout *layout)
|
||||
{
|
||||
delete layout->attrs;
|
||||
layout->format->Release();
|
||||
uiFree(layout->graphemes);
|
||||
uiFree(layout->text);
|
||||
uiFree(layout);
|
||||
}
|
||||
|
||||
|
||||
IDWriteTextLayout *prepareLayout(uiDrawTextLayout *layout, ID2D1RenderTarget *rt)
|
||||
{
|
||||
IDWriteTextLayout *dl;
|
||||
DWRITE_TEXT_RANGE range;
|
||||
IUnknown *unkBrush;
|
||||
HRESULT hr;
|
||||
|
||||
for (const struct layoutAttr &attr : *(layout->attrs)) {
|
||||
range.startPosition = layout->graphemes[attr.start];
|
||||
range.length = layout->graphemes[attr.end] - layout->graphemes[attr.start];
|
||||
switch (attr.type) {
|
||||
case layoutAttrColor:
|
||||
if (rt == NULL) // determining extents, not drawing
|
||||
break;
|
||||
unkBrush = mkSolidBrush(rt,
|
||||
attr.components[0],
|
||||
attr.components[1],
|
||||
attr.components[2],
|
||||
attr.components[3]);
|
||||
hr = dl->SetDrawingEffect(unkBrush, range);
|
||||
unkBrush->Release(); // associated with dl
|
||||
break;
|
||||
default:
|
||||
hr = E_FAIL;
|
||||
logHRESULT(L"invalid text attribute type", hr);
|
||||
}
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error adding attribute to text layout", hr);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return dl;
|
||||
}
|
||||
|
||||
|
||||
void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout)
|
||||
{
|
||||
IDWriteTextLayout *dl;
|
||||
D2D1_POINT_2F pt;
|
||||
ID2D1Brush *black;
|
||||
HRESULT hr;
|
||||
|
||||
// TODO document that fully opaque black is the default text color; figure out whether this is upheld in various scenarios on other platforms
|
||||
black = mkSolidBrush(c->rt, 0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
dl = prepareLayout(layout, c->rt);
|
||||
pt.x = x;
|
||||
pt.y = y;
|
||||
// TODO D2D1_DRAW_TEXT_OPTIONS_NO_SNAP?
|
||||
// TODO D2D1_DRAW_TEXT_OPTIONS_CLIP?
|
||||
// TODO when setting 8.1 as minimum, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT?
|
||||
c->rt->DrawTextLayout(pt, dl, black, D2D1_DRAW_TEXT_OPTIONS_NONE);
|
||||
dl->Release();
|
||||
|
||||
black->Release();
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a)
|
||||
{
|
||||
struct layoutAttr attr;
|
||||
|
||||
attr.type = layoutAttrColor;
|
||||
attr.start = startChar;
|
||||
attr.end = endChar;
|
||||
attr.components[0] = r;
|
||||
attr.components[1] = g;
|
||||
attr.components[2] = b;
|
||||
attr.components[3] = a;
|
||||
layout->attrs->push_back(attr);
|
||||
}
|
||||
@@ -0,0 +1,667 @@
|
||||
// 17 january 2017
|
||||
#include "uipriv_windows.hpp"
|
||||
#include "draw.hpp"
|
||||
|
||||
// TODO
|
||||
// - consider the warnings about antialiasing in the PadWrite sample
|
||||
// - if that's not a problem, do we have overlapping rects in the hittest sample? I can't tell...
|
||||
// - empty string: nLines == 1 and all checks out except extents has x == 0 when not left aligned
|
||||
// - paragraph alignment is subject to RTL mirroring; see if it is on other platforms
|
||||
// - add overhang info to metrics?
|
||||
|
||||
// TODO verify our renderer is correct, especially with regards to snapping
|
||||
|
||||
struct uiDrawTextLayout {
|
||||
IDWriteTextFormat *format;
|
||||
IDWriteTextLayout *layout;
|
||||
std::vector<backgroundFunc> *backgroundFuncs;
|
||||
UINT32 nLines;
|
||||
struct lineInfo *lineInfo;
|
||||
// for converting DirectWrite indices from/to byte offsets
|
||||
size_t *u8tou16;
|
||||
size_t nUTF8;
|
||||
size_t *u16tou8;
|
||||
size_t nUTF16;
|
||||
};
|
||||
|
||||
// TODO copy notes about DirectWrite DIPs being equal to Direct2D DIPs here
|
||||
|
||||
// typographic points are 1/72 inch; this parameter is 1/96 inch
|
||||
// fortunately Microsoft does this too, in https://msdn.microsoft.com/en-us/library/windows/desktop/dd371554%28v=vs.85%29.aspx
|
||||
#define pointSizeToDWriteSize(size) (size * (96.0 / 72.0))
|
||||
|
||||
struct lineInfo {
|
||||
size_t startPos; // in UTF-16 points
|
||||
size_t endPos;
|
||||
size_t newlineCount;
|
||||
double x;
|
||||
double y;
|
||||
double width;
|
||||
double height;
|
||||
double baseline;
|
||||
};
|
||||
|
||||
// this function is deeply indebted to the PadWrite sample: https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/Win7Samples/multimedia/DirectWrite/PadWrite/TextEditor.cpp
|
||||
static void computeLineInfo(uiDrawTextLayout *tl)
|
||||
{
|
||||
DWRITE_LINE_METRICS *dlm;
|
||||
size_t nextStart;
|
||||
UINT32 i, j;
|
||||
DWRITE_HIT_TEST_METRICS *htm;
|
||||
UINT32 nFragments, unused;
|
||||
HRESULT hr;
|
||||
|
||||
// TODO make sure this is legal; if not, switch to GetMetrics() and use its line count field instead
|
||||
hr = tl->layout->GetLineMetrics(NULL, 0, &(tl->nLines));
|
||||
// ugh, HRESULT_TO_WIN32() is an inline function and is not constexpr so we can't use switch here
|
||||
if (hr == S_OK) {
|
||||
// TODO what do we do here
|
||||
} else if (hr != E_NOT_SUFFICIENT_BUFFER)
|
||||
logHRESULT(L"error getting number of lines in IDWriteTextLayout", hr);
|
||||
tl->lineInfo = (struct lineInfo *) uiAlloc(tl->nLines * sizeof (struct lineInfo), "struct lineInfo[] (text layout)");
|
||||
|
||||
dlm = new DWRITE_LINE_METRICS[tl->nLines];
|
||||
// we can't pass NULL here; it outright crashes if we do
|
||||
// TODO verify the numbers haven't changed
|
||||
hr = tl->layout->GetLineMetrics(dlm, tl->nLines, &unused);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error getting IDWriteTextLayout line metrics", hr);
|
||||
|
||||
// assume the first line starts at position 0 and the string flow is incremental
|
||||
nextStart = 0;
|
||||
for (i = 0; i < tl->nLines; i++) {
|
||||
tl->lineInfo[i].startPos = nextStart;
|
||||
tl->lineInfo[i].endPos = nextStart + dlm[i].length;
|
||||
tl->lineInfo[i].newlineCount = dlm[i].newlineLength;
|
||||
nextStart = tl->lineInfo[i].endPos;
|
||||
|
||||
// a line can have multiple fragments; for example, if there's a bidirectional override in the middle of a line
|
||||
hr = tl->layout->HitTestTextRange(tl->lineInfo[i].startPos, (tl->lineInfo[i].endPos - tl->lineInfo[i].newlineCount) - tl->lineInfo[i].startPos,
|
||||
0, 0,
|
||||
NULL, 0, &nFragments);
|
||||
if (hr != S_OK && hr != E_NOT_SUFFICIENT_BUFFER)
|
||||
logHRESULT(L"error getting IDWriteTextLayout line fragment count", hr);
|
||||
htm = new DWRITE_HIT_TEST_METRICS[nFragments];
|
||||
// TODO verify unused == nFragments?
|
||||
hr = tl->layout->HitTestTextRange(tl->lineInfo[i].startPos, (tl->lineInfo[i].endPos - tl->lineInfo[i].newlineCount) - tl->lineInfo[i].startPos,
|
||||
0, 0,
|
||||
htm, nFragments, &unused);
|
||||
// TODO can this return E_NOT_SUFFICIENT_BUFFER again?
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error getting IDWriteTextLayout line fragment metrics", hr);
|
||||
// TODO verify htm.textPosition and htm.length against dtm[i]/tl->lineInfo[i]?
|
||||
tl->lineInfo[i].x = htm[0].left;
|
||||
tl->lineInfo[i].y = htm[0].top;
|
||||
// TODO does this not include trailing whitespace? I forget
|
||||
tl->lineInfo[i].width = htm[0].width;
|
||||
tl->lineInfo[i].height = htm[0].height;
|
||||
for (j = 1; j < nFragments; j++) {
|
||||
// this is correct even if the leftmost fragment on the line is RTL
|
||||
if (tl->lineInfo[i].x > htm[j].left)
|
||||
tl->lineInfo[i].x = htm[j].left;
|
||||
tl->lineInfo[i].width += htm[j].width;
|
||||
// TODO verify y and height haven't changed?
|
||||
}
|
||||
// TODO verify dlm[i].height == htm.height?
|
||||
delete[] htm;
|
||||
|
||||
// TODO on Windows 8.1 and/or 10 we can use DWRITE_LINE_METRICS1 to get specific info about the ascent and descent; do we have an alternative?
|
||||
// TODO and even on those platforms can we somehow split tyographic leading from spacing?
|
||||
// TODO and on that note, can we have both line spacing proportionally above and uniformly below?
|
||||
tl->lineInfo[i].baseline = dlm[i].baseline;
|
||||
}
|
||||
|
||||
delete[] dlm;
|
||||
}
|
||||
|
||||
// TODO should be const but then I can't operator[] on it; the real solution is to find a way to do designated array initializers in C++11 but I do not know enough C++ voodoo to make it work (it is possible but no one else has actually done it before)
|
||||
static std::map<uiDrawTextAlign, DWRITE_TEXT_ALIGNMENT> dwriteAligns = {
|
||||
{ uiDrawTextAlignLeft, DWRITE_TEXT_ALIGNMENT_LEADING },
|
||||
{ uiDrawTextAlignCenter, DWRITE_TEXT_ALIGNMENT_CENTER },
|
||||
{ uiDrawTextAlignRight, DWRITE_TEXT_ALIGNMENT_TRAILING },
|
||||
};
|
||||
|
||||
uiDrawTextLayout *uiDrawNewTextLayout(uiDrawTextLayoutParams *p)
|
||||
{
|
||||
uiDrawTextLayout *tl;
|
||||
WCHAR *wDefaultFamily;
|
||||
DWRITE_WORD_WRAPPING wrap;
|
||||
FLOAT maxWidth;
|
||||
HRESULT hr;
|
||||
|
||||
tl = uiNew(uiDrawTextLayout);
|
||||
|
||||
wDefaultFamily = toUTF16(p->DefaultFont->Family);
|
||||
hr = dwfactory->CreateTextFormat(
|
||||
wDefaultFamily, NULL,
|
||||
uiprivWeightToDWriteWeight(p->DefaultFont->Weight),
|
||||
uiprivItalicToDWriteStyle(p->DefaultFont->Italic),
|
||||
uiprivStretchToDWriteStretch(p->DefaultFont->Stretch),
|
||||
pointSizeToDWriteSize(p->DefaultFont->Size),
|
||||
// see http://stackoverflow.com/questions/28397971/idwritefactorycreatetextformat-failing and https://msdn.microsoft.com/en-us/library/windows/desktop/dd368203.aspx
|
||||
// TODO use the current locale?
|
||||
L"",
|
||||
&(tl->format));
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error creating IDWriteTextFormat", hr);
|
||||
hr = tl->format->SetTextAlignment(dwriteAligns[p->Align]);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error applying text layout alignment", hr);
|
||||
|
||||
hr = dwfactory->CreateTextLayout(
|
||||
(const WCHAR *) attrstrUTF16(p->String), attrstrUTF16Len(p->String),
|
||||
tl->format,
|
||||
// FLOAT is float, not double, so this should work... TODO
|
||||
FLT_MAX, FLT_MAX,
|
||||
&(tl->layout));
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error creating IDWriteTextLayout", hr);
|
||||
|
||||
// and set the width
|
||||
// this is the only wrapping mode (apart from "no wrap") available prior to Windows 8.1 (TODO verify this fact) (TODO this should be the default anyway)
|
||||
wrap = DWRITE_WORD_WRAPPING_WRAP;
|
||||
maxWidth = (FLOAT) (p->Width);
|
||||
if (p->Width < 0) {
|
||||
// TODO is this wrapping juggling even necessary?
|
||||
wrap = DWRITE_WORD_WRAPPING_NO_WRAP;
|
||||
// setting the max width in this case technically isn't needed since the wrap mode will simply ignore the max width, but let's do it just to be safe
|
||||
maxWidth = FLT_MAX; // see TODO above
|
||||
}
|
||||
hr = tl->layout->SetWordWrapping(wrap);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error setting IDWriteTextLayout word wrapping mode", hr);
|
||||
hr = tl->layout->SetMaxWidth(maxWidth);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error setting IDWriteTextLayout max layout width", hr);
|
||||
|
||||
attrstrToIDWriteTextLayoutAttrs(p, tl->layout, &(tl->backgroundFuncs));
|
||||
|
||||
computeLineInfo(tl);
|
||||
|
||||
// and finally copy the UTF-8/UTF-16 index conversion tables
|
||||
tl->u8tou16 = attrstrCopyUTF8ToUTF16(p->String, &(tl->nUTF8));
|
||||
tl->u16tou8 = attrstrCopyUTF16ToUTF8(p->String, &(tl->nUTF16));
|
||||
|
||||
// TODO can/should this be moved elsewhere?
|
||||
uiFree(wDefaultFamily);
|
||||
return tl;
|
||||
}
|
||||
|
||||
void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
|
||||
{
|
||||
uiFree(tl->u16tou8);
|
||||
uiFree(tl->u8tou16);
|
||||
uiFree(tl->lineInfo);
|
||||
delete tl->backgroundFuncs;
|
||||
tl->layout->Release();
|
||||
tl->format->Release();
|
||||
uiFree(tl);
|
||||
}
|
||||
|
||||
static HRESULT mkSolidBrush(ID2D1RenderTarget *rt, double r, double g, double b, double a, ID2D1SolidColorBrush **brush)
|
||||
{
|
||||
D2D1_BRUSH_PROPERTIES props;
|
||||
D2D1_COLOR_F color;
|
||||
|
||||
ZeroMemory(&props, sizeof (D2D1_BRUSH_PROPERTIES));
|
||||
props.opacity = 1.0;
|
||||
// identity matrix
|
||||
props.transform._11 = 1;
|
||||
props.transform._22 = 1;
|
||||
color.r = r;
|
||||
color.g = g;
|
||||
color.b = b;
|
||||
color.a = a;
|
||||
return rt->CreateSolidColorBrush(
|
||||
&color,
|
||||
&props,
|
||||
brush);
|
||||
}
|
||||
|
||||
static ID2D1SolidColorBrush *mustMakeSolidBrush(ID2D1RenderTarget *rt, double r, double g, double b, double a)
|
||||
{
|
||||
ID2D1SolidColorBrush *brush;
|
||||
HRESULT hr;
|
||||
|
||||
hr = mkSolidBrush(rt, r, g, b, a, &brush);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error creating solid brush", hr);
|
||||
return brush;
|
||||
}
|
||||
|
||||
// some of the stuff we want to do isn't possible with what DirectWrite provides itself; we need to do it ourselves
|
||||
// this is based on http://www.charlespetzold.com/blog/2014/01/Character-Formatting-Extensions-with-DirectWrite.html
|
||||
class textRenderer : public IDWriteTextRenderer {
|
||||
ULONG refcount;
|
||||
ID2D1RenderTarget *rt;
|
||||
BOOL snap;
|
||||
ID2D1SolidColorBrush *black;
|
||||
public:
|
||||
textRenderer(ID2D1RenderTarget *rt, BOOL snap, ID2D1SolidColorBrush *black)
|
||||
{
|
||||
this->refcount = 1;
|
||||
this->rt = rt;
|
||||
this->snap = snap;
|
||||
this->black = black;
|
||||
}
|
||||
|
||||
// IUnknown
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject)
|
||||
{
|
||||
if (ppvObject == NULL)
|
||||
return E_POINTER;
|
||||
if (riid == IID_IUnknown ||
|
||||
riid == __uuidof (IDWritePixelSnapping) ||
|
||||
riid == __uuidof (IDWriteTextRenderer)) {
|
||||
this->AddRef();
|
||||
*ppvObject = this;
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObject = NULL;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef(void)
|
||||
{
|
||||
this->refcount++;
|
||||
return this->refcount;
|
||||
}
|
||||
|
||||
virtual ULONG STDMETHODCALLTYPE Release(void)
|
||||
{
|
||||
this->refcount--;
|
||||
if (this->refcount == 0) {
|
||||
delete this;
|
||||
return 0;
|
||||
}
|
||||
return this->refcount;
|
||||
}
|
||||
|
||||
// IDWritePixelSnapping
|
||||
virtual HRESULT STDMETHODCALLTYPE GetCurrentTransform(void *clientDrawingContext, DWRITE_MATRIX *transform)
|
||||
{
|
||||
D2D1_MATRIX_3X2_F d2dtf;
|
||||
|
||||
if (transform == NULL)
|
||||
return E_POINTER;
|
||||
this->rt->GetTransform(&d2dtf);
|
||||
transform->m11 = d2dtf._11;
|
||||
transform->m12 = d2dtf._12;
|
||||
transform->m21 = d2dtf._21;
|
||||
transform->m22 = d2dtf._22;
|
||||
transform->dx = d2dtf._31;
|
||||
transform->dy = d2dtf._32;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetPixelsPerDip(void *clientDrawingContext, FLOAT *pixelsPerDip)
|
||||
{
|
||||
FLOAT dpix, dpiy;
|
||||
|
||||
if (pixelsPerDip == NULL)
|
||||
return E_POINTER;
|
||||
this->rt->GetDpi(&dpix, &dpiy);
|
||||
*pixelsPerDip = dpix / 96;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE IsPixelSnappingDisabled(void *clientDrawingContext, BOOL *isDisabled)
|
||||
{
|
||||
if (isDisabled == NULL)
|
||||
return E_POINTER;
|
||||
*isDisabled = !this->snap;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// IDWriteTextRenderer
|
||||
virtual HRESULT STDMETHODCALLTYPE DrawGlyphRun(void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, DWRITE_MEASURING_MODE measuringMode, const DWRITE_GLYPH_RUN *glyphRun, const DWRITE_GLYPH_RUN_DESCRIPTION *glyphRunDescription, IUnknown *clientDrawingEffect)
|
||||
{
|
||||
D2D1_POINT_2F baseline;
|
||||
textDrawingEffect *t = (textDrawingEffect *) clientDrawingEffect;
|
||||
ID2D1SolidColorBrush *brush;
|
||||
|
||||
baseline.x = baselineOriginX;
|
||||
baseline.y = baselineOriginY;
|
||||
brush = this->black;
|
||||
if (t != NULL && t->hasColor) {
|
||||
HRESULT hr;
|
||||
|
||||
hr = mkSolidBrush(this->rt, t->r, t->g, t->b, t->a, &brush);
|
||||
if (hr != S_OK)
|
||||
return hr;
|
||||
}
|
||||
this->rt->DrawGlyphRun(
|
||||
baseline,
|
||||
glyphRun,
|
||||
brush,
|
||||
measuringMode);
|
||||
if (t != NULL && t->hasColor)
|
||||
brush->Release();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE DrawInlineObject(void *clientDrawingContext, FLOAT originX, FLOAT originY, IDWriteInlineObject *inlineObject, BOOL isSideways, BOOL isRightToLeft, IUnknown *clientDrawingEffect)
|
||||
{
|
||||
if (inlineObject == NULL)
|
||||
return E_POINTER;
|
||||
return inlineObject->Draw(clientDrawingContext, this,
|
||||
originX, originY,
|
||||
isSideways, isRightToLeft,
|
||||
clientDrawingEffect);
|
||||
}
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE DrawStrikethrough(void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, const DWRITE_STRIKETHROUGH *strikethrough, IUnknown *clientDrawingEffect)
|
||||
{
|
||||
// we don't support strikethrough
|
||||
return E_UNEXPECTED;
|
||||
}
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE DrawUnderline(void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, const DWRITE_UNDERLINE *underline, IUnknown *clientDrawingEffect)
|
||||
{
|
||||
textDrawingEffect *t = (textDrawingEffect *) clientDrawingEffect;
|
||||
ID2D1SolidColorBrush *brush;
|
||||
D2D1_RECT_F rect;
|
||||
D2D1::Matrix3x2F pixeltf;
|
||||
FLOAT dpix, dpiy;
|
||||
D2D1_POINT_2F pt;
|
||||
|
||||
if (underline == NULL)
|
||||
return E_POINTER;
|
||||
if (t == NULL) // we can only get here through an underline
|
||||
return E_UNEXPECTED;
|
||||
brush = this->black;
|
||||
if (t->hasUnderlineColor) {
|
||||
HRESULT hr;
|
||||
|
||||
hr = mkSolidBrush(this->rt, t->ur, t->ug, t->ub, t->ua, &brush);
|
||||
if (hr != S_OK)
|
||||
return hr;
|
||||
} else if (t->hasColor) {
|
||||
// TODO formalize this rule
|
||||
HRESULT hr;
|
||||
|
||||
hr = mkSolidBrush(this->rt, t->r, t->g, t->b, t->a, &brush);
|
||||
if (hr != S_OK)
|
||||
return hr;
|
||||
}
|
||||
rect.left = baselineOriginX;
|
||||
rect.top = baselineOriginY + underline->offset;
|
||||
rect.right = rect.left + underline->width;
|
||||
rect.bottom = rect.top + underline->thickness;
|
||||
switch (t->u) {
|
||||
case uiDrawUnderlineStyleSingle:
|
||||
this->rt->FillRectangle(&rect, brush);
|
||||
break;
|
||||
case uiDrawUnderlineStyleDouble:
|
||||
// TODO do any of the matrix methods return errors?
|
||||
// TODO standardize double-underline shape across platforms? wavy underline shape?
|
||||
this->rt->GetTransform(&pixeltf);
|
||||
this->rt->GetDpi(&dpix, &dpiy);
|
||||
pixeltf = pixeltf * D2D1::Matrix3x2F::Scale(dpix / 96, dpiy / 96);
|
||||
pt.x = 0;
|
||||
pt.y = rect.top;
|
||||
pt = pixeltf.TransformPoint(pt);
|
||||
rect.top = (FLOAT) ((int) (pt.y + 0.5));
|
||||
pixeltf.Invert();
|
||||
pt = pixeltf.TransformPoint(pt);
|
||||
rect.top = pt.y;
|
||||
// first line
|
||||
rect.top -= underline->thickness;
|
||||
// and it seems we need to recompute this
|
||||
rect.bottom = rect.top + underline->thickness;
|
||||
this->rt->FillRectangle(&rect, brush);
|
||||
// second line
|
||||
rect.top += 2 * underline->thickness;
|
||||
rect.bottom = rect.top + underline->thickness;
|
||||
this->rt->FillRectangle(&rect, brush);
|
||||
break;
|
||||
case uiDrawUnderlineStyleSuggestion:
|
||||
{ // TODO get rid of the extra block
|
||||
// TODO properly clean resources on failure
|
||||
// TODO use fully qualified C overloads for all methods
|
||||
// TODO ensure all methods properly have errors handled
|
||||
ID2D1PathGeometry *path;
|
||||
ID2D1GeometrySink *sink;
|
||||
double amplitude, period, xOffset, yOffset;
|
||||
double t;
|
||||
bool first = true;
|
||||
HRESULT hr;
|
||||
|
||||
hr = d2dfactory->CreatePathGeometry(&path);
|
||||
if (hr != S_OK)
|
||||
return hr;
|
||||
hr = path->Open(&sink);
|
||||
if (hr != S_OK)
|
||||
return hr;
|
||||
amplitude = underline->thickness;
|
||||
period = 5 * underline->thickness;
|
||||
xOffset = baselineOriginX;
|
||||
yOffset = baselineOriginY + underline->offset;
|
||||
for (t = 0; t < underline->width; t++) {
|
||||
double x, angle, y;
|
||||
D2D1_POINT_2F pt;
|
||||
|
||||
x = t + xOffset;
|
||||
angle = 2 * uiPi * fmod(x, period) / period;
|
||||
y = amplitude * sin(angle) + yOffset;
|
||||
pt.x = x;
|
||||
pt.y = y;
|
||||
if (first) {
|
||||
sink->BeginFigure(pt, D2D1_FIGURE_BEGIN_HOLLOW);
|
||||
first = false;
|
||||
} else
|
||||
sink->AddLine(pt);
|
||||
}
|
||||
sink->EndFigure(D2D1_FIGURE_END_OPEN);
|
||||
hr = sink->Close();
|
||||
if (hr != S_OK)
|
||||
return hr;
|
||||
sink->Release();
|
||||
this->rt->DrawGeometry(path, brush, underline->thickness);
|
||||
path->Release();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (t->hasUnderlineColor || t->hasColor)
|
||||
brush->Release();
|
||||
return S_OK;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO this ignores clipping?
|
||||
void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
|
||||
{
|
||||
D2D1_POINT_2F pt;
|
||||
ID2D1SolidColorBrush *black;
|
||||
textRenderer *renderer;
|
||||
HRESULT hr;
|
||||
|
||||
for (const auto &f : *(tl->backgroundFuncs))
|
||||
f(c, tl, x, y);
|
||||
|
||||
// TODO document that fully opaque black is the default text color; figure out whether this is upheld in various scenarios on other platforms
|
||||
// TODO figure out if this needs to be cleaned out
|
||||
black = mustMakeSolidBrush(c->rt, 0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
#define renderD2D 0
|
||||
#define renderOur 1
|
||||
#if renderD2D
|
||||
pt.x = x;
|
||||
pt.y = y;
|
||||
// TODO D2D1_DRAW_TEXT_OPTIONS_NO_SNAP?
|
||||
// TODO D2D1_DRAW_TEXT_OPTIONS_CLIP?
|
||||
// TODO LONGTERM when setting 8.1 as minimum (TODO verify), D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT?
|
||||
// TODO what is our pixel snapping setting related to the OPTIONS enum values?
|
||||
c->rt->DrawTextLayout(pt, tl->layout, black, D2D1_DRAW_TEXT_OPTIONS_NONE);
|
||||
#endif
|
||||
#if renderD2D && renderOur
|
||||
// draw ours semitransparent so we can check
|
||||
// TODO get the actual color Charles Petzold uses and use that
|
||||
black->Release();
|
||||
black = mustMakeSolidBrush(c->rt, 1.0, 0.0, 0.0, 0.75);
|
||||
#endif
|
||||
#if renderOur
|
||||
renderer = new textRenderer(c->rt,
|
||||
TRUE, // TODO FALSE for no-snap?
|
||||
black);
|
||||
hr = tl->layout->Draw(NULL,
|
||||
renderer,
|
||||
x, y);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error drawing IDWriteTextLayout", hr);
|
||||
renderer->Release();
|
||||
#endif
|
||||
|
||||
black->Release();
|
||||
}
|
||||
|
||||
// TODO for a single line the height includes the leading; should it? TextEdit on OS X always includes the leading and/or paragraph spacing, otherwise Klee won't work...
|
||||
// TODO width does not include trailing whitespace
|
||||
void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height)
|
||||
{
|
||||
DWRITE_TEXT_METRICS metrics;
|
||||
HRESULT hr;
|
||||
|
||||
hr = tl->layout->GetMetrics(&metrics);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error getting IDWriteTextLayout layout metrics", hr);
|
||||
*width = metrics.width;
|
||||
// TODO make sure the behavior of this on empty strings is the same on all platforms (ideally should be 0-width, line height-height; TODO note this in the docs too)
|
||||
*height = metrics.height;
|
||||
}
|
||||
|
||||
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
|
||||
{
|
||||
return tl->nLines;
|
||||
}
|
||||
|
||||
// DirectWrite doesn't provide a direct way to do this, so we have to do this manually
|
||||
// TODO does that comment still apply here or to the code at the top of this file?
|
||||
void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
|
||||
{
|
||||
*start = tl->lineInfo[line].startPos;
|
||||
*start = tl->u16tou8[*start];
|
||||
*end = tl->lineInfo[line].endPos - tl->lineInfo[line].newlineCount;
|
||||
*end = tl->u16tou8[*end];
|
||||
}
|
||||
|
||||
void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
|
||||
{
|
||||
m->X = tl->lineInfo[line].x;
|
||||
m->Y = tl->lineInfo[line].y;
|
||||
m->Width = tl->lineInfo[line].width;
|
||||
m->Height = tl->lineInfo[line].height;
|
||||
|
||||
// TODO rename tl->lineInfo[line].baseline to .baselineOffset or something of the sort to make its meaning more clear
|
||||
m->BaselineY = tl->lineInfo[line].y + tl->lineInfo[line].baseline;
|
||||
m->Ascent = tl->lineInfo[line].baseline;
|
||||
m->Descent = tl->lineInfo[line].height - tl->lineInfo[line].baseline;
|
||||
m->Leading = 0; // TODO
|
||||
|
||||
m->ParagraphSpacingBefore = 0; // TODO
|
||||
m->LineHeightSpace = 0; // TODO
|
||||
m->LineSpacing = 0; // TODO
|
||||
m->ParagraphSpacing = 0; // TODO
|
||||
}
|
||||
|
||||
// this algorithm comes from Microsoft's PadWrite sample, following TextEditor::SetSelectionFromPoint()
|
||||
// TODO go back through all of these and make sure we convert coordinates properly
|
||||
// TODO same for OS X
|
||||
void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, size_t *pos, int *line)
|
||||
{
|
||||
DWRITE_HIT_TEST_METRICS m;
|
||||
BOOL trailing, inside;
|
||||
size_t p;
|
||||
UINT32 i;
|
||||
HRESULT hr;
|
||||
|
||||
hr = tl->layout->HitTestPoint(x, y,
|
||||
&trailing, &inside,
|
||||
&m);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error hit-testing IDWriteTextLayout", hr);
|
||||
p = m.textPosition;
|
||||
// on a trailing hit, align to the nearest cluster
|
||||
if (trailing) {
|
||||
DWRITE_HIT_TEST_METRICS m2;
|
||||
FLOAT x, y; // crashes if I skip these :/
|
||||
|
||||
hr = tl->layout->HitTestTextPosition(m.textPosition, trailing,
|
||||
&x, &y, &m2);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error aligning trailing hit to nearest cluster", hr);
|
||||
p = m2.textPosition + m2.length;
|
||||
}
|
||||
*pos = tl->u16tou8[p];
|
||||
|
||||
for (i = 0; i < tl->nLines; i++) {
|
||||
double ltop, lbottom;
|
||||
|
||||
ltop = tl->lineInfo[i].y;
|
||||
lbottom = ltop + tl->lineInfo[i].height;
|
||||
// y will already >= ltop at this point since the past lbottom should == ltop
|
||||
if (y < lbottom)
|
||||
break;
|
||||
}
|
||||
if (i == tl->nLines)
|
||||
i--;
|
||||
*line = i;
|
||||
}
|
||||
|
||||
double uiDrawTextLayoutByteLocationInLine(uiDrawTextLayout *tl, size_t pos, int line)
|
||||
{
|
||||
BOOL trailing;
|
||||
DWRITE_HIT_TEST_METRICS m;
|
||||
FLOAT x, y;
|
||||
HRESULT hr;
|
||||
|
||||
if (line < 0 || line >= tl->nLines)
|
||||
return -1;
|
||||
pos = tl->u8tou16[pos];
|
||||
// note: >, not >=, because the position at endPos is valid!
|
||||
if (pos < tl->lineInfo[line].startPos || pos > tl->lineInfo[line].endPos)
|
||||
return -1;
|
||||
// this behavior seems correct
|
||||
// there's also PadWrite's TextEditor::GetCaretRect() but that requires state...
|
||||
// TODO where does this fail?
|
||||
trailing = FALSE;
|
||||
if (pos != 0 && pos != tl->nUTF16 && pos == tl->lineInfo[line].endPos) {
|
||||
pos--;
|
||||
trailing = TRUE;
|
||||
}
|
||||
hr = tl->layout->HitTestTextPosition(pos, trailing,
|
||||
&x, &y, &m);
|
||||
if (hr != S_OK)
|
||||
logHRESULT(L"error calling IDWriteTextLayout::HitTestTextPosition()", hr);
|
||||
return x;
|
||||
}
|
||||
|
||||
void caretDrawParams(uiDrawContext *c, double height, struct caretDrawParams *p)
|
||||
{
|
||||
DWORD caretWidth;
|
||||
|
||||
// there seems to be no defined caret color
|
||||
// the best I can come up with is "inverts colors underneath" (according to https://msdn.microsoft.com/en-us/library/windows/desktop/ms648397(v=vs.85).aspx) which I have no idea how to do (TODO)
|
||||
// just return black for now
|
||||
p->r = 0.0;
|
||||
p->g = 0.0;
|
||||
p->b = 0.0;
|
||||
p->a = 1.0;
|
||||
|
||||
if (SystemParametersInfoW(SPI_GETCARETWIDTH, 0, &caretWidth, 0) == 0)
|
||||
// don't log the failure, fall back gracefully
|
||||
// the instruction to use this comes from https://msdn.microsoft.com/en-us/library/windows/desktop/ms648399(v=vs.85).aspx
|
||||
// and we have to assume GetSystemMetrics() always succeeds, so
|
||||
caretWidth = GetSystemMetrics(SM_CXBORDER);
|
||||
// TODO make this a function and split it out of areautil.cpp
|
||||
{
|
||||
FLOAT dpix, dpiy;
|
||||
|
||||
// TODO can we pass NULL for dpiy?
|
||||
c->rt->GetDpi(&dpix, &dpiy);
|
||||
// see https://msdn.microsoft.com/en-us/library/windows/desktop/dd756649%28v=vs.85%29.aspx (and others; search "direct2d mouse")
|
||||
p->width = ((double) (caretWidth * 96)) / dpix;
|
||||
}
|
||||
// and there doesn't seem to be this either... (TODO check what PadWrite does?)
|
||||
p->xoff = 0;
|
||||
}
|
||||
Reference in new issue
Block a user