diff --git a/example/demo.c b/example/demo.c index e51e86b..3f2f9c9 100644 --- a/example/demo.c +++ b/example/demo.c @@ -808,12 +808,80 @@ void freeDemoData(struct NVGcontext* vg, struct DemoData* data) nvgDeleteImage(vg, data->images[i]); } +void drawParagraph(struct NVGcontext* vg, float x, float y, float width, float height, float mx, float my) +{ + struct NVGtextRow rows[3]; + struct NVGglyphPosition glyphs[100]; + const char* text = "This is longer chunk of text.\n \n Would have used lorem ipsum but she was busy jumping over the lazy dog with the fox and all the men who came to the aid of the party."; + const char* start; + const char* end; + int nrows, i, nglyphs, j; + float lineh; + float caretx, px; + + nvgSave(vg); + + nvgFontSize(vg, 18.0f); + nvgFontFace(vg, "sans"); + nvgTextAlign(vg, NVG_ALIGN_LEFT|NVG_ALIGN_TOP); + nvgTextMetrics(vg, NULL, NULL, &lineh); + +/* nvgBeginPath(vg); + nvgFillColor(vg, nvgRGBA(255,255,0,128)); + nvgRect(vg, x, y, width, height); + nvgFill(vg);*/ + + // The text break API can be used to fill a large buffer of rows, + // or to iterate over the text just few lines (or just one) at a time. + // The "next" variable of the last returned item tells where to continue. + start = text; + end = text + strlen(text); + while ((nrows = nvgTextBreakLines(vg, start, end, width, rows, 3))) { + for (i = 0; i < nrows; i++) { + struct NVGtextRow* row = &rows[i]; + int hit = mx > x && mx < (x+width) && my >= y && my < (y+lineh); + + nvgBeginPath(vg); + nvgFillColor(vg, nvgRGBA(255,255,255,hit?64:8)); + nvgRect(vg, x, y, row->width, lineh); + nvgFill(vg); + + nvgFillColor(vg, nvgRGBA(255,255,255,255)); + nvgText(vg, x, y, row->start, row->end); + + if (hit) { + caretx = (mx < x+row->width/2) ? x : x+row->width; + px = x; + nglyphs = nvgTextGlyphPositions(vg, row->start, row->end, x, y, glyphs, 100); + for (j = 0; j < nglyphs; j++) { + float x0 = glyphs[j].x; + float x1 = (j+1 < nglyphs) ? glyphs[j+1].x : x+row->width; + float gx = (x0 + x1)/2; + if (mx > px && mx <= gx) + caretx = glyphs[j].x; + px = gx; + } + nvgBeginPath(vg); + nvgFillColor(vg, nvgRGBA(255,192,0,255)); + nvgRect(vg, caretx, y, 2, lineh); + nvgFill(vg); + } + y += lineh; + } + // Keep going... + start = rows[nrows-1].next; + } + + nvgRestore(vg); +} + void renderDemo(struct NVGcontext* vg, float mx, float my, float width, float height, float t, int blowup, struct DemoData* data) { float x,y,popy; drawEyes(vg, width - 250, 50, 150, 100, mx, my, t); + drawParagraph(vg, width - 450, 50, 150, 100, mx, my); drawGraph(vg, 0, height/2, width, height/2, t); drawColorwheel(vg, width - 300, height - 300, 250.0f, 250.0f, t); diff --git a/premake4.lua b/premake4.lua index 795f769..123482d 100644 --- a/premake4.lua +++ b/premake4.lua @@ -13,6 +13,14 @@ solution "nanovg" files { "src/*.c" } targetdir("build") + configuration "Debug" + defines { "DEBUG" } + flags { "Symbols", "ExtraWarnings"} + + configuration "Release" + defines { "NDEBUG" } + flags { "Optimize", "ExtraWarnings"} + project "example_gl2" kind "ConsoleApp" language "C" diff --git a/src/fontstash.h b/src/fontstash.h index cd8d132..4bf488e 100644 --- a/src/fontstash.h +++ b/src/fontstash.h @@ -68,10 +68,12 @@ struct FONSquad struct FONStextIter { float x, y, scale, spacing; + unsigned int codepoint; short isize, iblur; struct FONSfont* font; struct FONSglyph* prevGlyph; const char* str; + const char* next; const char* end; unsigned int utf8state; }; @@ -1292,7 +1294,9 @@ int fonsTextIterInit(struct FONScontext* stash, struct FONStextIter* iter, iter->y = y; iter->spacing = state->spacing; iter->str = str; + iter->next = str; iter->end = end; + iter->codepoint = 0; return 1; } @@ -1300,24 +1304,24 @@ int fonsTextIterInit(struct FONScontext* stash, struct FONStextIter* iter, int fonsTextIterNext(struct FONScontext* stash, struct FONStextIter* iter, struct FONSquad* quad) { struct FONSglyph* glyph = NULL; - unsigned int codepoint = 0; - const char* str = iter->str; + const char* str = iter->next; + iter->str = iter->next; if (str == iter->end) return 0; for (; str != iter->end; str++) { - if (fons__decutf8(&iter->utf8state, &codepoint, *(const unsigned char*)str)) + if (fons__decutf8(&iter->utf8state, &iter->codepoint, *(const unsigned char*)str)) continue; str++; // Get glyph and quad - glyph = fons__getGlyph(stash, iter->font, codepoint, iter->isize, iter->iblur); + glyph = fons__getGlyph(stash, iter->font, iter->codepoint, iter->isize, iter->iblur); if (glyph != NULL) fons__getQuad(stash, iter->font, iter->prevGlyph, glyph, iter->scale, iter->spacing, &iter->x, &iter->y, quad); iter->prevGlyph = glyph; break; } - iter->str = str; + iter->next = str; return 1; } diff --git a/src/nanovg.c b/src/nanovg.c index b8946f8..4a1cda9 100644 --- a/src/nanovg.c +++ b/src/nanovg.c @@ -181,7 +181,7 @@ error: static void nvg__setDevicePixelRatio(struct NVGcontext* ctx, float ratio) { - ctx->tessTol = 0.25f * 4.0f / ratio; + ctx->tessTol = 1.0f / ratio; ctx->distTol = 0.01f / ratio; ctx->fringeWidth = 1.0f / ratio; ctx->devicePxRatio = ratio; @@ -978,7 +978,6 @@ static void nvg__tesselateBezier(struct NVGcontext* ctx, { float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; float dx,dy,d2,d3; - float tol = 0.5f; if (level > 10) return; @@ -996,7 +995,7 @@ static void nvg__tesselateBezier(struct NVGcontext* ctx, d2 = nvg__absf(((x2 - x4) * dy - (y2 - y4) * dx)); d3 = nvg__absf(((x3 - x4) * dy - (y3 - y4) * dx)); - if ((d2 + d3)*(d2 + d3) < tol * (dx*dx + dy*dy)) { + if ((d2 + d3)*(d2 + d3) < ctx->tessTol * (dx*dx + dy*dy)) { nvg__addPoint(ctx, x4, y4, type); return; } @@ -1985,6 +1984,205 @@ float nvgText(struct NVGcontext* ctx, float x, float y, const char* string, cons return iter.x; } +int nvgTextGlyphPositions(struct NVGcontext* ctx, const char* string, const char* end, float x, float y, struct NVGglyphPosition* positions, int maxPositions) +{ + struct NVGstate* state = nvg__getState(ctx); + struct FONStextIter iter; + struct FONSquad q; + int npos = 0; + float px; + + if (state->fontId == FONS_INVALID) return 0; + + if (end == NULL) + end = string + strlen(string); + + if (string == end) + return 0; + + fonsSetSize(ctx->fs, state->fontSize); + fonsSetSpacing(ctx->fs, state->letterSpacing); + fonsSetBlur(ctx->fs, state->fontBlur); + fonsSetAlign(ctx->fs, state->textAlign); + fonsSetFont(ctx->fs, state->fontId); + + px = x; + fonsTextIterInit(ctx->fs, &iter, x, y, string, end); + while (fonsTextIterNext(ctx->fs, &iter, &q)) { + positions[npos].str = iter.str; + positions[npos].x = px; + px = iter.x; + npos++; + if (npos >= maxPositions) + break; + } + + return npos; +} + +enum NVGcodepointType { + NVG_SPACE, + NVG_NEWLINE, + NVG_CHAR, +}; + +int nvgTextBreakLines(struct NVGcontext* ctx, const char* string, const char* end, float maxRowWidth, struct NVGtextRow* rows, int maxRows) +{ + struct NVGstate* state = nvg__getState(ctx); + struct FONStextIter iter; + struct FONSquad q; + int nrows = 0; + float rowStartX = 0; + float rowWidth = 0; + const char* rowStart = NULL; + const char* rowEnd = NULL; + const char* wordStart = NULL; + float wordStartX = 0; + const char* breakEnd = NULL; + float breakWidth = 0; + int type = NVG_SPACE, ptype = NVG_SPACE; + unsigned int pcodepoint = 0; + + if (maxRows == 0) return 0; + if (state->fontId == FONS_INVALID) return 0; + + if (end == NULL) + end = string + strlen(string); + + if (string == end) return 0; + + fonsSetSize(ctx->fs, state->fontSize); + fonsSetSpacing(ctx->fs, state->letterSpacing); + fonsSetBlur(ctx->fs, state->fontBlur); + fonsSetAlign(ctx->fs, state->textAlign); + fonsSetFont(ctx->fs, state->fontId); + + fonsTextIterInit(ctx->fs, &iter, 0, 0, string, end); + while (fonsTextIterNext(ctx->fs, &iter, &q)) { + switch (iter.codepoint) { + case 9: // \t + case 11: // \v + case 12: // \f + case 32: // space + case 0x00a0: // NBSP + type = NVG_SPACE; + break; + case 10: // \n + type = pcodepoint == 13 ? NVG_SPACE : NVG_NEWLINE; + break; + case 13: // \r + type = pcodepoint == 10 ? NVG_SPACE : NVG_NEWLINE; + break; + case 0x0085: // NEL + type = NVG_NEWLINE; + break; + default: + type = NVG_CHAR; + break; + } + + if (type == NVG_NEWLINE) { + // Always handle new lines. + rows[nrows].start = rowStart != NULL ? rowStart : iter.str; + rows[nrows].end = rowEnd != NULL ? rowEnd : iter.str; + rows[nrows].width = rowWidth; + rows[nrows].next = iter.next; + nrows++; + if (nrows >= maxRows) + return nrows; + // Indicate to skip the white space at the beginning of the row. + rowStart = NULL; + rowEnd = NULL; + rowWidth = 0; + } else { + if (rowStart == NULL) { + // Skip white space until the beginning of the line + if (type == NVG_CHAR) { + // The current char is the row so far + rowStartX = q.x0; + rowStart = iter.str; + rowEnd = iter.next; + rowWidth = q.x1 - rowStartX; + wordStart = iter.str; + wordStartX = q.x0; + // Set null break point + breakEnd = rowStart; + breakWidth = 0.0; + } + } else { + float nextWidth = q.x1 - rowStartX; + + if (nextWidth > maxRowWidth) { + // The run length is too long, need to break to new line. + if (breakEnd == rowStart) { + // The current word is longer than the row length, just break it from here. + rows[nrows].start = rowStart; + rows[nrows].end = iter.str; + rows[nrows].width = rowWidth; + rows[nrows].next = iter.str; + nrows++; + if (nrows >= maxRows) + return nrows; + rowStartX = q.x0; + rowStart = iter.str; + rowEnd = iter.next; + rowWidth = q.x1 - rowStartX; + wordStart = iter.str; + wordStartX = q.x0; + } else { + // Break the line from the end of the last word, and start new line from the begining of the new. + rows[nrows].start = rowStart; + rows[nrows].end = breakEnd; + rows[nrows].width = breakWidth; + rows[nrows].next = wordStart; + nrows++; + if (nrows >= maxRows) + return nrows; + rowStartX = wordStartX; + rowStart = wordStart; + rowEnd = iter.next; + rowWidth = q.x1 - rowStartX; + // No change to the word start + } + // Set null break point + breakEnd = rowStart; + breakWidth = 0.0; + } + + // track last non-white space character + if (type == NVG_CHAR) { + rowEnd = iter.next; + rowWidth = q.x1 - rowStartX; + } + // track last end of a word + if (ptype == NVG_CHAR && (type == NVG_SPACE || type == NVG_SPACE)) { + breakEnd = iter.str; + breakWidth = rowWidth; + } + // track last beginning of a word + if ((ptype == NVG_SPACE || ptype == NVG_SPACE) && type == NVG_CHAR) { + wordStart = iter.str; + wordStartX = q.x0; + } + } + } + + pcodepoint = iter.codepoint; + ptype = type; + } + + // Break the line from the end of the last word, and start new line from the begining of the new. + if (rowStart != NULL) { + rows[nrows].start = rowStart; + rows[nrows].end = rowEnd; + rows[nrows].width = rowWidth; + rows[nrows].next = end; + nrows++; + } + + return nrows; +} + float nvgTextBounds(struct NVGcontext* ctx, const char* string, const char* end, float* bounds) { struct NVGstate* state = nvg__getState(ctx); @@ -2000,7 +2198,7 @@ float nvgTextBounds(struct NVGcontext* ctx, const char* string, const char* end, return fonsTextBounds(ctx->fs, string, end, bounds); } -void nvgVertMetrics(struct NVGcontext* ctx, float* ascender, float* descender, float* lineh) +void nvgTextMetrics(struct NVGcontext* ctx, float* ascender, float* descender, float* lineh) { struct NVGstate* state = nvg__getState(ctx); diff --git a/src/nanovg.h b/src/nanovg.h index efe3d7d..1c603ea 100644 --- a/src/nanovg.h +++ b/src/nanovg.h @@ -419,8 +419,21 @@ float nvgTextBounds(struct NVGcontext* ctx, const char* string, const char* end, // Returns the vertical metrics based on the current text style. // Current transform does not affect the measured values. -void nvgVertMetrics(struct NVGcontext* ctx, float* ascender, float* descender, float* lineh); +void nvgTextMetrics(struct NVGcontext* ctx, float* ascender, float* descender, float* lineh); +struct NVGglyphPosition { + const char* str; + float x; +}; +int nvgTextGlyphPositions(struct NVGcontext* ctx, const char* string, const char* end, float x, float y, struct NVGglyphPosition* positions, int maxPositions); + +struct NVGtextRow { + const char* start; + const char* end; + const char* next; + float width; +}; +int nvgTextBreakLines(struct NVGcontext* ctx, const char* string, const char* end, float maxRowWidth, struct NVGtextRow* rows, int maxRows); // // Internal Render API