From 6ccf436206396a2a3b61400d18d00e1df18b74b6 Mon Sep 17 00:00:00 2001 From: Pietro Gagliardi Date: Fri, 20 Jan 2017 16:36:44 -0500 Subject: [PATCH] Implemented Pango text metrics and expanded the drawtext basic page to draw metrics. Works on both Pango and OS X; DirectWrite comes next. --- darwin/drawtext.m | 2 + examples/drawtext/basic.c | 187 +++++++++++++++++++++++++++++++++++++- unix/drawtext.c | 60 +++++++++++- 3 files changed, 241 insertions(+), 8 deletions(-) diff --git a/darwin/drawtext.m b/darwin/drawtext.m index a8a34542..a38c152d 100644 --- a/darwin/drawtext.m +++ b/darwin/drawtext.m @@ -246,6 +246,8 @@ void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y) } // 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) { *width = tl->size.width; diff --git a/examples/drawtext/basic.c b/examples/drawtext/basic.c index 2d3d7f3d..04ea42e1 100644 --- a/examples/drawtext/basic.c +++ b/examples/drawtext/basic.c @@ -30,10 +30,93 @@ static uiAttributedString *attrstr; #define margins 10 +static uiBox *panel; +static uiCheckbox *showExtents; +static uiCheckbox *showLineBounds; +static uiCheckbox *showLineGuides; + +// TODO should this be const? +static double strokeDashes[] = { 5, 2 }; +// TODO this should be const +static uiDrawStrokeParams strokeParams = { + .Cap = uiDrawLineCapFlat, + .Join = uiDrawLineJoinMiter, + .Thickness = 1, + .MiterLimit = uiDrawDefaultMiterLimit, + .Dashes = strokeDashes, + .NumDashes = 2, + .DashPhase = 0, +}; + +// TODO should be const? +static uiDrawBrush fillBrushes[4] = { + { + .Type = uiDrawBrushTypeSolid, + .R = 1.0, + .G = 0.0, + .B = 0.0, + .A = 0.5, + }, + { + .Type = uiDrawBrushTypeSolid, + .R = 0.0, + .G = 1.0, + .B = 0.0, + .A = 0.5, + }, + { + .Type = uiDrawBrushTypeSolid, + .R = 0.0, + .G = 0.0, + .B = 1.0, + .A = 0.5, + }, + { + .Type = uiDrawBrushTypeSolid, + .R = 0.0, + .G = 1.0, + .B = 1.0, + .A = 0.5, + }, +}; +// TODO come up with better colors +static uiDrawBrush strokeBrushes[3] = { + // baseline + { + .Type = uiDrawBrushTypeSolid, + .R = 0.5, + .G = 0.5, + .B = 0.0, + .A = 0.75, + }, + // ascent + { + .Type = uiDrawBrushTypeSolid, + .R = 1.0, + .G = 0.0, + .B = 1.0, + .A = 0.75, + }, + // descent + { + .Type = uiDrawBrushTypeSolid, + .R = 0.5, + .G = 0.75, + .B = 1.0, + .A = 0.75, + }, +}; + static void draw(uiAreaDrawParams *p) { uiDrawPath *path; uiDrawTextLayout *layout; + uiDrawBrush b; + + b.Type = uiDrawBrushTypeSolid; + + // only clip the text, not the guides + uiDrawSave(p->Context); path = uiDrawNewPath(uiDrawFillModeWinding); uiDrawPathAddRectangle(path, margins, margins, @@ -44,33 +127,131 @@ static void draw(uiAreaDrawParams *p) uiDrawFreePath(path); // TODO get rid of this later +#if 0 path = uiDrawNewPath(uiDrawFillModeWinding); uiDrawPathAddRectangle(path, -100, -100, p->AreaWidth * 2, p->AreaHeight * 2); uiDrawPathEnd(path); - uiDrawBrush b; - b.Type = uiDrawBrushTypeSolid; b.R = 0.0; b.G = 1.0; b.B = 0.0; b.A = 1.0; uiDrawFill(p->Context, path, &b); uiDrawFreePath(path); +#endif layout = uiDrawNewTextLayout(attrstr, &defaultFont, p->AreaWidth - 2 * margins); uiDrawText(p->Context, layout, margins, margins); + + uiDrawRestore(p->Context); + + if (uiCheckboxChecked(showExtents)) { + double width, height; + + uiDrawTextLayoutExtents(layout, &width, &height); + path = uiDrawNewPath(uiDrawFillModeWinding); + uiDrawPathAddRectangle(path, margins, margins, + width, height); + uiDrawPathEnd(path); + b.R = 1.0; + b.G = 0.0; + b.B = 1.0; + b.A = 0.5; + uiDrawStroke(p->Context, path, &b, &strokeParams); + uiDrawFreePath(path); + } + + if (uiCheckboxChecked(showLineBounds) || uiCheckboxChecked(showLineGuides)) { + uiDrawTextLayoutLineMetrics m; + int i, n; + int fill = 0; + + n = uiDrawTextLayoutNumLines(layout); + for (i = 0; i < n; i++) { + uiDrawTextLayoutLineGetMetrics(layout, i, &m); + + if (uiCheckboxChecked(showLineBounds)) { + path = uiDrawNewPath(uiDrawFillModeWinding); + uiDrawPathAddRectangle(path, margins + m.X, margins + m.Y, + m.Width, m.Height); + uiDrawPathEnd(path); + uiDrawFill(p->Context, path, fillBrushes + fill); + uiDrawFreePath(path); + fill = (fill + 1) % 4; + } + if (uiCheckboxChecked(showLineGuides)) { + // baseline + path = uiDrawNewPath(uiDrawFillModeWinding); + uiDrawPathNewFigure(path, + margins + m.X, + margins + m.BaselineY); + uiDrawPathLineTo(path, + margins + m.X + m.Width, + margins + m.BaselineY); + uiDrawPathEnd(path); + uiDrawStroke(p->Context, path, &(strokeBrushes[0]), &strokeParams); + uiDrawFreePath(path); + + // ascent line + path = uiDrawNewPath(uiDrawFillModeWinding); + uiDrawPathNewFigure(path, + margins + m.X, + margins + m.BaselineY - m.Ascent); + uiDrawPathLineTo(path, + margins + m.X + m.Width, + margins + m.BaselineY - m.Ascent); + uiDrawPathEnd(path); + uiDrawStroke(p->Context, path, &(strokeBrushes[1]), &strokeParams); + uiDrawFreePath(path); + + // descent line + path = uiDrawNewPath(uiDrawFillModeWinding); + uiDrawPathNewFigure(path, + margins + m.X, + margins + m.BaselineY + m.Descent); + uiDrawPathLineTo(path, + margins + m.X + m.Width, + margins + m.BaselineY + m.Descent); + uiDrawPathEnd(path); + uiDrawStroke(p->Context, path, &(strokeBrushes[2]), &strokeParams); + uiDrawFreePath(path); + } + } + } + uiDrawFreeTextLayout(layout); } static struct example basicExample; +// TODO share? +static void checkboxChecked(uiCheckbox *c, void *data) +{ + redraw(); +} + +static uiCheckbox *newCheckbox(const char *text) +{ + uiCheckbox *c; + + c = uiNewCheckbox(text); + uiCheckboxOnToggled(c, checkboxChecked, NULL); + uiBoxAppend(panel, uiControl(c), 0); + return c; +} + struct example *mkBasicExample(void) { + panel = uiNewVerticalBox(); + showExtents = newCheckbox("Show Layout Extents"); + showLineBounds = newCheckbox("Show Line Bounds"); + showLineGuides = newCheckbox("Show Line Guides"); + basicExample.name = "Basic Paragraph of Text"; - basicExample.panel = uiControl(uiNewVerticalBox()); + basicExample.panel = uiControl(panel); basicExample.draw = draw; attrstr = uiNewAttributedString(text); diff --git a/unix/drawtext.c b/unix/drawtext.c index 64ae9928..217b3c48 100644 --- a/unix/drawtext.c +++ b/unix/drawtext.c @@ -4,6 +4,7 @@ struct uiDrawTextLayout { PangoLayout *layout; + uiDrawTextLayoutLineMetrics *lineMetrics; }; // See https://developer.gnome.org/pango/1.30/pango-Cairo-Rendering.html#pango-Cairo-Rendering.description @@ -35,6 +36,55 @@ static const PangoStretch pangoStretches[] = { [uiDrawTextStretchUltraExpanded] = PANGO_STRETCH_ULTRA_EXPANDED, }; +// 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 = pango_layout_get_line_count(tl->layout); + 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)); + 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(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width) { uiDrawTextLayout *tl; @@ -76,11 +126,14 @@ uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescripto // TODO attributes + computeLineMetrics(tl); + return tl; } void uiDrawFreeTextLayout(uiDrawTextLayout *tl) { + uiFree(tl->lineMetrics); g_object_unref(tl->layout); uiFree(tl); } @@ -112,15 +165,12 @@ void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start pll = pango_layout_get_line_readonly(tl->layout, line); *start = pll->start_index; *end = pll->start_index + pll->length; - // TODO unref? + // TODO unref pll? } void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m) { - PangoLayoutLine *pll; - - pll = pango_layout_get_line_readonly(tl->layout, line); - // TODO unref? + *m = tl->lineMetrics[line]; } // TODO