diff --git a/nanovega.d b/nanovega.d index 4e50df1..921cf55 100644 --- a/nanovega.d +++ b/nanovega.d @@ -445,51 +445,22 @@ The following code illustrates the OpenGL state touched by the rendering code: Note: currently only solid color fill is supported for text. - path_recording = - ## Recording and Replaying Pathes + font_stash = + ## Low-Level Font Engine (FontStash) - $(WARNING This API is hightly experimental, and is subject to change. - While I will try to keep it compatible in future NanoVega - versions, no promises are made. Also note that NanoVega - rendering is quite fast, so you prolly don't need this - functionality. If you really want to render-once-and-copy, - consider rendering to FBO, and use imaging API to blit - FBO texture instead. Note that NanoVega supports alot of - blit/copy modes.) + FontStash is used to load fonts, to manage font atlases, and to get various text metrics. + You don't need any graphics context to use FontStash, so you can do things like text + layouting outside of your rendering code. Loaded fonts are refcounted, so it is cheap + to create new FontStash, copy fonts from NanoVega context into it, and use that new + FontStash to do some UI layouting, for example. Also note that you can get text metrics + without creating glyph bitmaps (by using [FONSTextBoundsIterator], for example); this way + you don't need to waste CPU and memory resources to render unneeded images into font atlas, + and you can layout alot of text very fast. - It is posible to record render commands and replay them later. This will allow - you to skip possible time-consuming tesselation stage. Potential uses of this - feature is, for example, rendering alot of similar complex paths, like game - tiles, or enemy sprites. + Note that "FontStash" is abbrevated as "FONS". So when you see some API that contains + word "fons" in it, this is not a typo, and it should not read "font" intead. - Path replaying has some limitations, though: you cannot change stroke width, - fringe size, tesselation tolerance, or rescale path. But you can change fill - color/pattern, stroke color, translate and/or rotate saved paths. - - Note that text rendering commands are not saved, as technically text rendering - is not a path. - - To translate or rotate a record, use [affineGPU] API call. - - To record render commands, you must create new path set with [newPathSet] - function, then start recording with [startRecording]. You can cancel current - recording with [cancelRecording], or commit (save) recording with [stopRecording]. - - You can resume recording with [startRecording] after [stopRecording] call. - Calling [cancelRecording] will cancel only current recording session (i.e. it - will forget everything from the very latest [startRecording], not the whole - record). - - Finishing frame with [endFrame] will automatically commit current recording, and - calling [cancelFrame] will cancel recording by calling [cancelRecording]. - - Note that commit recording will clear current picking scene (but cancelling won't). - - Calling [startRecording] without commiting or cancelling recoriding will commit. - - $(WARNING Text output is not recorded now. Neither is scissor, so if you are using - scissoring or text in your paths (UI, for example), things will not - work as you may expect.) + TODO for Ketmar: write some nice example code here, and finish documenting FontStash API. */ module arsd.nanovega; private: @@ -1566,10 +1537,49 @@ struct NVGpathCache { public alias NVGContext = NVGcontextinternal*; /// FontStash context +/// Group: font_stash public alias FONSContext = FONScontextInternal*; /// Returns FontStash context of the given NanoVega context. -public FONSContext fonsContext (NVGContext ctx) { return (ctx !is null && ctx.contextAlive ? ctx.fs : null); } +/// Group: font_stash +public FONSContext fonsContext (NVGContext ctx) pure nothrow @trusted @nogc { pragma(inline, true); return (ctx !is null && ctx.contextAlive ? ctx.fs : null); } + +/// Returns scale that should be applied to FontStash parameters due to matrix transformations on the context (or 1) +/// Group: font_stash +public float fonsScale (NVGContext ctx) /*pure*/ nothrow @trusted @nogc { + pragma(inline, true); + return (ctx !is null && ctx.contextAlive && ctx.nstates > 0 ? nvg__getFontScale(&ctx.states.ptr[ctx.nstates-1])*ctx.devicePxRatio : 1); +} + +/// Setup FontStash from the given NanoVega context font parameters. Note that this will apply transformation scale too. +/// Returns `false` if FontStash or NanoVega context is not active. +/// Group: font_stash +public bool setupFonsFrom (FONSContext stash, NVGContext ctx) nothrow @trusted @nogc { + if (stash is null || ctx is null || !ctx.contextAlive || ctx.nstates == 0) return false; + NVGstate* state = nvg__getState(ctx); + immutable float scale = nvg__getFontScale(state)*ctx.devicePxRatio; + stash.size = state.fontSize*scale; + stash.spacing = state.letterSpacing*scale; + stash.blur = state.fontBlur*scale; + stash.textAlign = state.textAlign; + stash.fontId = state.fontId; + return true; +} + +/// Setup NanoVega context font parameters from the given FontStash. Note that NanoVega can apply transformation scale later. +/// Returns `false` if FontStash or NanoVega context is not active. +/// Group: font_stash +public bool setupCtxFrom (NVGContext ctx, FONSContext stash) nothrow @trusted @nogc { + if (stash is null || ctx is null || !ctx.contextAlive || ctx.nstates == 0) return false; + NVGstate* state = nvg__getState(ctx); + immutable float scale = nvg__getFontScale(state)*ctx.devicePxRatio; + state.fontSize = stash.size; + state.letterSpacing = stash.spacing; + state.fontBlur = stash.blur; + state.textAlign = stash.textAlign; + state.fontId = stash.fontId; + return true; +} /** Bezier curve rasterizer. * @@ -1634,11 +1644,11 @@ public void pickmode (NVGContext ctx, uint v) nothrow @trusted @nogc { pragma(in // tesselator options -/// +/// Get current Bezier tesselation mode. See [NVGTesselation]. /// Group: context_management public NVGTesselation tesselation (NVGContext ctx) pure nothrow @trusted @nogc { pragma(inline, true); return (ctx !is null ? ctx.tesselatortype : NVGTesselation.DeCasteljau); } -/// +/// Set current Bezier tesselation mode. See [NVGTesselation]. /// Group: context_management public void tesselation (NVGContext ctx, NVGTesselation v) nothrow @trusted @nogc { pragma(inline, true); if (ctx !is null) ctx.tesselatortype = v; } @@ -1920,13 +1930,12 @@ NVGContext createInternal (NVGparams* params) nothrow @trusted @nogc { memset(&fontParams, 0, fontParams.sizeof); fontParams.width = NVG_INIT_FONTIMAGE_SIZE; fontParams.height = NVG_INIT_FONTIMAGE_SIZE; - fontParams.flags = FONS_ZERO_TOPLEFT; + fontParams.flags = FONSParams.Flag.ZeroTopLeft; fontParams.renderCreate = null; fontParams.renderUpdate = null; - debug(nanovega) fontParams.renderDraw = null; fontParams.renderDelete = null; fontParams.userPtr = null; - ctx.fs = FONSContext.createInternal(fontParams); + ctx.fs = FONSContext.create(fontParams); if (ctx.fs is null) goto error; // create font texture @@ -2119,8 +2128,8 @@ public void endFrame (NVGContext ctx) nothrow @trusted @nogc { // ////////////////////////////////////////////////////////////////////////// // // Recording and Replaying Pathes -/// Saved path set. -/// Group: path_recording +// Saved path set. +// Group: path_recording public alias NVGPathSet = NVGPathSetS*; @@ -2373,8 +2382,8 @@ void appendCurrentPathToCache (NVGContext ctx, NVGPathSet svp, in ref NVGPaint p } } -/// Create new empty path set. -/// Group: path_recording +// Create new empty path set. +// Group: path_recording public NVGPathSet newPathSet (NVGContext ctx) nothrow @trusted @nogc { import core.stdc.stdlib : malloc; import core.stdc.string : memset; @@ -2386,12 +2395,12 @@ public NVGPathSet newPathSet (NVGContext ctx) nothrow @trusted @nogc { return res; } -/// Is the given path set empty? Empty path set can be `null`. -/// Group: path_recording +// Is the given path set empty? Empty path set can be `null`. +// Group: path_recording public bool empty (NVGPathSet svp) pure nothrow @safe @nogc { pragma(inline, true); return (svp is null || svp.nnodes == 0); } -/// Clear path set contents. Will release $(B some) allocated memory (this function is meant to clear something that will be reused). -/// Group: path_recording +// Clear path set contents. Will release $(B some) allocated memory (this function is meant to clear something that will be reused). +// Group: path_recording public void clear (NVGPathSet svp) nothrow @trusted @nogc { if (svp !is null) { import core.stdc.stdlib : free; @@ -2400,8 +2409,8 @@ public void clear (NVGPathSet svp) nothrow @trusted @nogc { } } -/// Destroy path set (frees all allocated memory). -/// Group: path_recording +// Destroy path set (frees all allocated memory). +// Group: path_recording public void kill (ref NVGPathSet svp) nothrow @trusted @nogc { if (svp !is null) { import core.stdc.stdlib : free; @@ -2413,8 +2422,8 @@ public void kill (ref NVGPathSet svp) nothrow @trusted @nogc { } } -/// Start path recording. [svp] should be alive until recording is cancelled or stopped. -/// Group: path_recording +// Start path recording. [svp] should be alive until recording is cancelled or stopped. +// Group: path_recording public void startRecording (NVGContext ctx, NVGPathSet svp) nothrow @trusted @nogc { if (svp !is null && svp.svctx !is ctx) assert(0, "NanoVega: cannot share path set between contexts"); ctx.stopRecording(); @@ -2423,7 +2432,7 @@ public void startRecording (NVGContext ctx, NVGPathSet svp) nothrow @trusted @no ctx.recblockdraw = false; } -/** Start path recording. [svp] should be alive until recording is cancelled or stopped. +/* Start path recording. [svp] should be alive until recording is cancelled or stopped. * * This will block all rendering, so you can call your rendering functions to record paths without actual drawing. * Commiting or cancelling will re-enable rendering. @@ -2439,8 +2448,8 @@ public void startBlockingRecording (NVGContext ctx, NVGPathSet svp) nothrow @tru ctx.recblockdraw = true; } -/// Commit recorded paths. It is safe to call this when recording is not started. -/// Group: path_recording +// Commit recorded paths. It is safe to call this when recording is not started. +// Group: path_recording public void stopRecording (NVGContext ctx) nothrow @trusted @nogc { if (ctx.recset !is null && ctx.recset.svctx !is ctx) assert(0, "NanoVega: cannot share path set between contexts"); if (ctx.recset !is null) ctx.recset.takeCurrentPickScene(ctx); @@ -2449,8 +2458,8 @@ public void stopRecording (NVGContext ctx) nothrow @trusted @nogc { ctx.recblockdraw = false; } -/// Cancel path recording. -/// Group: path_recording +// Cancel path recording. +// Group: path_recording public void cancelRecording (NVGContext ctx) nothrow @trusted @nogc { if (ctx.recset !is null) { if (ctx.recset.svctx !is ctx) assert(0, "NanoVega: cannot share path set between contexts"); @@ -2463,7 +2472,7 @@ public void cancelRecording (NVGContext ctx) nothrow @trusted @nogc { ctx.recblockdraw = false; } -/** Replay saved path set. +/* Replay saved path set. * * Replaying record while you're recording another one is undefined behavior. * @@ -4152,7 +4161,7 @@ void nvg__pathWinding (NVGContext ctx, NVGWinding winding) nothrow @trusted @nog path.mWinding = winding; } -float nvg__getAverageScale() (in auto ref NVGMatrix t) nothrow @trusted @nogc { +float nvg__getAverageScale() (in auto ref NVGMatrix t) /*pure*/ nothrow @trusted @nogc { immutable float sx = nvg__sqrtf(t.mat.ptr[0]*t.mat.ptr[0]+t.mat.ptr[2]*t.mat.ptr[2]); immutable float sy = nvg__sqrtf(t.mat.ptr[1]*t.mat.ptr[1]+t.mat.ptr[3]*t.mat.ptr[3]); return (sx+sy)*0.5f; @@ -8148,7 +8157,7 @@ public: /// Is this outline empty? @property empty () const pure { pragma(inline, true); return (dsaddr == 0 || ds.ccount == 0); } - /// Returns umber of commands in outline. + /// Returns number of commands in outline. @property int length () const pure { pragma(inline, true); return (dsaddr ? ds.ccount : 0); } /// Returns "flattened" path. Flattened path consists of only two commands kinds: MoveTo and LineTo. @@ -8385,7 +8394,7 @@ float nvg__quantize (float a, float d) pure nothrow @safe @nogc { return (cast(int)(a/d+0.5f))*d; } -float nvg__getFontScale (NVGstate* state) nothrow @safe @nogc { +float nvg__getFontScale (NVGstate* state) /*pure*/ nothrow @safe @nogc { pragma(inline, true); return nvg__min(nvg__quantize(nvg__getAverageScale(state.xform), 0.01f), 4.0f); } @@ -8473,7 +8482,7 @@ public float text(T) (NVGContext ctx, float x, float y, const(T)[] str) nothrow verts = nvg__allocTempVerts(ctx, cverts); if (verts is null) return x; - if (!iter.setup(ctx.fs, x*scale, y*scale, str, FONS_GLYPH_BITMAP_REQUIRED)) return x; + if (!iter.setup(ctx.fs, x*scale, y*scale, str, FONSBitmapFlag.Required)) return x; prevIter = iter; while (iter.next(q)) { float[4*2] c = void; @@ -8601,7 +8610,7 @@ if (isAnyCharType!T && isGoodPositionDelegate!DG) ctx.fs.textAlign = state.textAlign; ctx.fs.fontId = state.fontId; - if (!iter.setup(ctx.fs, x*scale, y*scale, str, FONS_GLYPH_BITMAP_OPTIONAL)) return npos; + if (!iter.setup(ctx.fs, x*scale, y*scale, str, FONSBitmapFlag.Optional)) return npos; prevIter = iter; while (iter.next(q)) { if (iter.prevGlyphIndex < 0) { // can not retrieve glyph? @@ -8712,7 +8721,7 @@ if (isAnyCharType!T && isGoodRowDelegate!(T, DG)) } Phase phase = Phase.SkipBlanks; // don't skip blanks on first line - if (!iter.setup(ctx.fs, 0, 0, str, FONS_GLYPH_BITMAP_OPTIONAL)) return 0; + if (!iter.setup(ctx.fs, 0, 0, str, FONSBitmapFlag.Optional)) return 0; prevIter = iter; while (iter.next(q)) { if (iter.prevGlyphIndex < 0) { // can not retrieve glyph? @@ -8884,7 +8893,7 @@ if (isAnyCharType!T && isGoodRowDelegate!(T, DG)) public struct TextBoundsIterator { private: NVGContext ctx; - FonsTextBoundsIterator fsiter; // fontstash iterator + FONSTextBoundsIterator fsiter; // fontstash iterator float scale, invscale, xscaled, yscaled; // font settings float fsSize, fsSpacing, fsBlur; @@ -8892,9 +8901,11 @@ private: NVGTextAlign fsAlign; public: - this (NVGContext actx, float ax, float ay) nothrow @trusted @nogc { reset(actx, ax, ay); } + /// Setups iteration. Takes current font parameters from the given NanoVega context. + this (NVGContext actx, float ax=0, float ay=0) nothrow @trusted @nogc { reset(actx, ax, ay); } - void reset (NVGContext actx, float ax, float ay) nothrow @trusted @nogc { + /// Resets iteration. Takes current font parameters from the given NanoVega context. + void reset (NVGContext actx, float ax=0, float ay=0) nothrow @trusted @nogc { fsiter = fsiter.init; this = this.init; if (actx is null) return; @@ -9231,47 +9242,47 @@ version(nanovg_use_freetype_ii) { // ////////////////////////////////////////////////////////////////////////// // //version = nanovg_ft_mono; -enum FONS_INVALID = -1; +/// Invald font id. +/// Group: font_stash +public enum FONS_INVALID = -1; -alias FONSflags = uint; -enum /*FONSflags*/ : uint { - FONS_ZERO_TOPLEFT = 1U<<0, - FONS_ZERO_BOTTOMLEFT = 1U<<1, +public enum FONSBitmapFlag : uint { + Required = 0, + Optional = 1, } -alias FONSglyphBitmap = uint; -enum /*FONSglyphBitmap*/ : uint { - FONS_GLYPH_BITMAP_OPTIONAL = 1, - FONS_GLYPH_BITMAP_REQUIRED = 2, -} - -alias FONSerrorCode = int; -enum /*FONSerrorCode*/ : int { - // Font atlas is full. - FONS_ATLAS_FULL = 1, - // Scratch memory used to render glyphs is full, requested size reported in 'val', you may need to bump up FONS_SCRATCH_BUF_SIZE. - FONS_SCRATCH_FULL = 2, - // Calls to fonsPushState has created too large stack, if you need deep state stack bump up FONS_MAX_STATES. - FONS_STATES_OVERFLOW = 3, - // Trying to pop too many states fonsPopState(). - FONS_STATES_UNDERFLOW = 4, +public enum FONSError : int { + NoError = 0, + AtlasFull = 1, // Font atlas is full. + ScratchFull = 2, // Scratch memory used to render glyphs is full, requested size reported in 'val', you may need to bump up FONS_SCRATCH_BUF_SIZE. + StatesOverflow = 3, // Calls to fonsPushState has created too large stack, if you need deep state stack bump up FONS_MAX_STATES. + StatesUnderflow = 4, // Trying to pop too many states fonsPopState(). } +/// Initial parameters for new FontStash. +/// Group: font_stash public struct FONSParams { + enum Flag : uint { + ZeroTopLeft = 0U, // default + ZeroBottomLeft = 1U, + } int width, height; - FONSflags flags; + Flag flags = Flag.ZeroTopLeft; void* userPtr; bool function (void* uptr, int width, int height) nothrow @trusted @nogc renderCreate; int function (void* uptr, int width, int height) nothrow @trusted @nogc renderResize; void function (void* uptr, int* rect, const(ubyte)* data) nothrow @trusted @nogc renderUpdate; void function (void* uptr) nothrow @trusted @nogc renderDelete; + @property bool isZeroTopLeft () const pure nothrow @trusted @nogc { pragma(inline, true); return ((flags&Flag.ZeroBottomLeft) == 0); } } +//TODO: document this public struct FONSQuad { float x0=0, y0=0, s0=0, t0=0; float x1=0, y1=0, s1=0, t1=0; } +//TODO: document this public struct FONSTextIter(CT) if (isAnyCharType!CT) { alias CharType = CT; float x=0, y=0, nextx=0, nexty=0, scale=0, spacing=0; @@ -9283,19 +9294,19 @@ public struct FONSTextIter(CT) if (isAnyCharType!CT) { const(CT)* s; // string const(CT)* n; // next const(CT)* e; // end - FONSglyphBitmap bitmapOption; + FONSBitmapFlag bitmapOption; static if (is(CT == char)) { uint utf8state; } - this (FONSContext astash, float ax, float ay, const(CharType)[] astr, FONSglyphBitmap abitmapOption) nothrow @trusted @nogc { setup(astash, ax, ay, astr, abitmapOption); } + this (FONSContext astash, float ax, float ay, const(CharType)[] astr, FONSBitmapFlag abitmapOption) nothrow @trusted @nogc { setup(astash, ax, ay, astr, abitmapOption); } ~this () nothrow @trusted @nogc { pragma(inline, true); static if (is(CT == char)) utf8state = 0; s = n = e = null; } @property const(CT)* stringp () const pure nothrow @trusted @nogc { pragma(inline, true); return s; } @property const(CT)* nextp () const pure nothrow @trusted @nogc { pragma(inline, true); return n; } @property const(CT)* endp () const pure nothrow @trusted @nogc { pragma(inline, true); return e; } - bool setup (FONSContext astash, float ax, float ay, const(CharType)[] astr, FONSglyphBitmap abitmapOption) nothrow @trusted @nogc { + bool setup (FONSContext astash, float ax, float ay, const(CharType)[] astr, FONSBitmapFlag abitmapOption) nothrow @trusted @nogc { import core.stdc.string : memset; memset(&this, 0, this.sizeof); @@ -10372,9 +10383,9 @@ void kill (ref FONSfont* font) nothrow @trusted @nogc { struct FONSstate { int font; NVGTextAlign talign; - float size; - float blur; - float spacing; + float size = 0; + float blur = 0; + float spacing = 0; } @@ -10558,7 +10569,9 @@ void kill (ref FONSAtlas atlas) nothrow @trusted @nogc { // ////////////////////////////////////////////////////////////////////////// // -private struct FONScontextInternal { +/// FontStash context (internal definition). Don't use it derectly, it was made public only to generate documentation. +/// Group: font_stash +public struct FONScontextInternal { private: FONSParams params; float itw, ith; @@ -10575,7 +10588,7 @@ private: FONSstate[FONS_MAX_STATES] states; int nstates; - void delegate (int error, int val) nothrow @trusted @nogc handleError; + void delegate (FONSError error, int val) nothrow @trusted @nogc handleError; @disable this (this); void opAssign() (in auto ref FONScontextInternal ctx) { static assert(0, "FONS copying is not allowed"); } @@ -10790,8 +10803,118 @@ private: return g; } + void clear () nothrow @trusted @nogc { + import core.stdc.stdlib : free; + + if (params.renderDelete !is null) params.renderDelete(params.userPtr); + foreach (immutable int i; 0..nfonts) fonts[i].kill(); + + if (atlas !is null) atlas.kill(); + if (fonts !is null) free(fonts); + if (texData !is null) free(texData); + if (scratch !is null) free(scratch); + if (hashidx !is null) free(hashidx); + } + + // add font from another fontstash + int addCookedFont (FONSfont* font) nothrow @trusted @nogc { + if (font is null || font.fdata is null) return FONS_INVALID; + font.fdata.incref(); + auto res = addFontWithData(font.name[0..font.namelen], font.fdata, !font.font.mono); + if (res == FONS_INVALID) font.fdata.decref(); // oops + return res; + } + + // fdata refcount must be already increased; it won't be changed + int addFontWithData (const(char)[] name, FONSfontData* fdata, bool defAA) nothrow @trusted @nogc { + int i, ascent, descent, fh, lineGap; + + if (name.length == 0 || strequci(name, NoAlias)) return FONS_INVALID; + if (name.length > 32767) return FONS_INVALID; + if (fdata is null) return FONS_INVALID; + + // find a font with the given name + int newidx; + FONSfont* oldfont = null; + int oldidx = findNameInHash(name); + if (oldidx != FONS_INVALID) { + // replacement font + oldfont = fonts[oldidx]; + newidx = oldidx; + } else { + // new font, allocate new bucket + newidx = -1; + } + + newidx = allocFontAt(newidx); + FONSfont* font = fonts[newidx]; + font.setName(name); + font.lut.ptr[0..FONS_HASH_LUT_SIZE] = -1; // init hash lookup + font.fdata = fdata; // set the font data (don't change reference count) + fons__tt_setMono(&this, &font.font, !defAA); + + // init font + nscratch = 0; + if (!fons__tt_loadFont(&this, &font.font, fdata.data, fdata.dataSize)) { + // we promised to not free data on error, so just clear the data store (it will be freed by the caller) + font.fdata = null; + font.kill(); + if (oldidx != FONS_INVALID) { + assert(oldidx == newidx); + fonts[oldidx] = oldfont; + } else { + assert(newidx == nfonts-1); + fonts[newidx] = null; + --nfonts; + } + return FONS_INVALID; + } else { + // free old font data, if any + if (oldfont !is null) oldfont.kill(); + } + + // add font to name hash + if (oldidx == FONS_INVALID) addIndexToHash(newidx); + + // store normalized line height + // the real line height is got by multiplying the lineh by font size + fons__tt_getFontVMetrics(&font.font, &ascent, &descent, &lineGap); + fh = ascent-descent; + font.ascender = cast(float)ascent/cast(float)fh; + font.descender = cast(float)descent/cast(float)fh; + font.lineh = cast(float)(fh+lineGap)/cast(float)fh; + + //{ import core.stdc.stdio; printf("created font [%.*s] (idx=%d)...\n", cast(uint)name.length, name.ptr, idx); } + return newidx; + } + + // isize: size*10 + float getVertAlign (FONSfont* font, NVGTextAlign talign, short isize) pure nothrow @trusted @nogc { + if (params.isZeroTopLeft) { + final switch (talign.vertical) { + case NVGTextAlign.V.Top: return font.ascender*cast(float)isize/10.0f; + case NVGTextAlign.V.Middle: return (font.ascender+font.descender)/2.0f*cast(float)isize/10.0f; + case NVGTextAlign.V.Baseline: return 0.0f; + case NVGTextAlign.V.Bottom: return font.descender*cast(float)isize/10.0f; + } + } else { + final switch (talign.vertical) { + case NVGTextAlign.V.Top: return -font.ascender*cast(float)isize/10.0f; + case NVGTextAlign.V.Middle: return -(font.ascender+font.descender)/2.0f*cast(float)isize/10.0f; + case NVGTextAlign.V.Baseline: return 0.0f; + case NVGTextAlign.V.Bottom: return -font.descender*cast(float)isize/10.0f; + } + } + assert(0); + } + public: - static FONSContext createInternal() (auto ref FONSParams params) nothrow @trusted @nogc { + /** Create new FontStash context. It can be destroyed with `fs.kill()` later. + * + * Note that if you don't plan to rasterize glyphs (i.e. you will use created + * FontStash only to measure text), you can simply pass `FONSParams.init`). + */ + static FONSContext create() (in auto ref FONSParams params) nothrow @trusted @nogc { import core.stdc.string : memcpy; FONSContext stash = null; @@ -10802,6 +10925,8 @@ public: memset(stash, 0, FONScontextInternal.sizeof); memcpy(&stash.params, ¶ms, params.sizeof); + if (stash.params.width < 1) stash.params.width = 32; + if (stash.params.height < 1) stash.params.height = 32; // allocate scratch buffer stash.scratch = cast(ubyte*)malloc(FONS_SCRATCH_BUF_SIZE); @@ -10847,19 +10972,7 @@ public: } public: - private void clear () nothrow @trusted @nogc { - import core.stdc.stdlib : free; - - if (params.renderDelete !is null) params.renderDelete(params.userPtr); - foreach (immutable int i; 0..nfonts) fonts[i].kill(); - - if (atlas !is null) atlas.kill(); - if (fonts !is null) free(fonts); - if (texData !is null) free(texData); - if (scratch !is null) free(scratch); - if (hashidx !is null) free(hashidx); - } - + /// Add fallback font (FontStash will try to find missing glyph in all fallback fonts before giving up). bool addFallbackFont (int base, int fallback) nothrow @trusted @nogc { FONSfont* baseFont = fonts[base]; if (baseFont !is null && baseFont.nfallbacks < FONS_MAX_FALLBACKS) { @@ -10869,26 +10982,27 @@ public: return false; } - @property void size (float size) nothrow @trusted @nogc { pragma(inline, true); getState.size = size; } - @property float size () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.size; } + @property void size (float size) nothrow @trusted @nogc { pragma(inline, true); getState.size = size; } /// Set current font size. + @property float size () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.size; } /// Get current font size. - @property void spacing (float spacing) nothrow @trusted @nogc { pragma(inline, true); getState.spacing = spacing; } - @property float spacing () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.spacing; } + @property void spacing (float spacing) nothrow @trusted @nogc { pragma(inline, true); getState.spacing = spacing; } /// Set current letter spacing. + @property float spacing () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.spacing; } /// Get current letter spacing. - @property void blur (float blur) nothrow @trusted @nogc { pragma(inline, true); getState.blur = blur; } - @property float blur () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.blur; } + @property void blur (float blur) nothrow @trusted @nogc { pragma(inline, true); getState.blur = blur; } /// Set current letter blur. + @property float blur () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.blur; } /// Get current letter blur. - @property void textAlign (NVGTextAlign talign) nothrow @trusted @nogc { pragma(inline, true); getState.talign = talign; } - @property NVGTextAlign textAlign () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.talign; } + @property void textAlign (NVGTextAlign talign) nothrow @trusted @nogc { pragma(inline, true); getState.talign = talign; } /// Set current text align. + @property NVGTextAlign textAlign () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.talign; } /// Get current text align. - @property void fontId (int font) nothrow @trusted @nogc { pragma(inline, true); getState.font = font; } - @property int fontId () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.font; } + @property void fontId (int font) nothrow @trusted @nogc { pragma(inline, true); getState.font = font; } /// Set current font id. + @property int fontId () const pure nothrow @trusted @nogc { pragma(inline, true); return getState.font; } /// Get current font id. - @property void fontId (const(char)[] name) nothrow @trusted @nogc { pragma(inline, true); getState.font = getFontByName(name); } + @property void fontId (const(char)[] name) nothrow @trusted @nogc { pragma(inline, true); getState.font = getFontByName(name); } /// Set current font using its name. + /// Check if FontStash has a font with the given name loaded. bool hasFont (const(char)[] name) const pure nothrow @trusted @nogc { pragma(inline, true); return (getFontByName(name) >= 0); } - // get AA for current font or for the specified font + /// Get AA for the current font, or for the specified font. bool getFontAA (int font=-1) nothrow @trusted @nogc { FONSstate* state = getState; if (font < 0) font = state.font; @@ -10897,9 +11011,10 @@ public: return (f !is null ? !f.font.mono : false); } + /// Push current state. Returns `false` if state stack overflowed. bool pushState () nothrow @trusted @nogc { if (nstates >= FONS_MAX_STATES) { - if (handleError !is null) handleError(FONS_STATES_OVERFLOW, 0); + if (handleError !is null) handleError(FONSError.StatesOverflow, 0); return false; } if (nstates > 0) { @@ -10910,15 +11025,17 @@ public: return true; } + /// Pop current state. Returns `false` if state stack underflowed. bool popState () nothrow @trusted @nogc { if (nstates <= 1) { - if (handleError !is null) handleError(FONS_STATES_UNDERFLOW, 0); + if (handleError !is null) handleError(FONSError.StatesUnderflow, 0); return false; } --nstates; return true; } + /// Clear current state (i.e. set it to some sane defaults). void clearState () nothrow @trusted @nogc { FONSstate* state = getState; state.size = 12.0f; @@ -10930,7 +11047,20 @@ public: private enum NoAlias = ":noaa"; - // defAA: antialias flag for fonts without ":noaa" + /** Add font to FontStash. + * + * Load scalable font from disk, and add it to FontStash. If you will try to load a font + * with same name and path several times, FontStash will load it only once. Also, you can + * load new disk font for any existing logical font. + * + * Params: + * name = logical font name, that will be used to select this font later. + * path = path to disk file with your font. + * defAA = should FontStash use antialiased font rasterizer? + * + * Returns: + * font id or [FONS_INVALID]. + */ int addFont (const(char)[] name, const(char)[] path, bool defAA=false) nothrow @trusted { if (path.length == 0 || name.length == 0 || strequci(name, NoAlias)) return FONS_INVALID; if (path.length > 32768) return FONS_INVALID; // arbitrary limit @@ -11065,8 +11195,25 @@ public: return res; } - // This will not free data on error! + /** Add font to FontStash, using data from memory. + * + * And already loaded font to FontStash. You can replace existing logical fonts. + * But note that you can't remove logical font by passing "empty" data. + * + * $(WARNING If [FONS_INVALID] returned, `data` won't be freed even if `freeData` is `true`.) + * + * Params: + * name = logical font name, that will be used to select this font later. + * data = font data. + * dataSize = font data size. + * freeData = should FontStash take ownership of the font data? + * defAA = should FontStash use antialiased font rasterizer? + * + * Returns: + * font id or [FONS_INVALID]. + */ int addFontMem (const(char)[] name, ubyte* data, int dataSize, bool freeData, bool defAA=false) nothrow @trusted @nogc { + if (data is null || dataSize < 16) return FONS_INVALID; FONSfontData* fdata = fons__createFontData(data, dataSize, freeData); fdata.incref(); auto res = addFontWithData(name, fdata, defAA); @@ -11078,8 +11225,11 @@ public: return res; } - // Add fonts from another font stash. - // This is more effective than reloading fonts, 'cause font data will be shared. + /** Add fonts from another FontStash. + * + * This is more effective (and faster) than reloading fonts, because internally font data + * is reference counted. + */ void addFontsFrom (FONSContext source) nothrow @trusted @nogc { if (source is null) return; foreach (FONSfont* font; source.fonts[0..source.nfonts]) { @@ -11100,87 +11250,14 @@ public: } } - // Add font from another fontstash. - private int addCookedFont (FONSfont* font) nothrow @trusted @nogc { - if (font is null || font.fdata is null) return FONS_INVALID; - font.fdata.incref(); - auto res = addFontWithData(font.name[0..font.namelen], font.fdata, !font.font.mono); - if (res == FONS_INVALID) font.fdata.decref(); // oops - return res; - } - - // fdata refcount must be already increased; it won't be changed - private int addFontWithData (const(char)[] name, FONSfontData* fdata, bool defAA) nothrow @trusted @nogc { - int i, ascent, descent, fh, lineGap; - - if (name.length == 0 || strequci(name, NoAlias)) return FONS_INVALID; - if (name.length > 32767) return FONS_INVALID; - if (fdata is null) return FONS_INVALID; - - // find a font with the given name - int newidx; - FONSfont* oldfont = null; - int oldidx = findNameInHash(name); - if (oldidx != FONS_INVALID) { - // replacement font - oldfont = fonts[oldidx]; - newidx = oldidx; - } else { - // new font, allocate new bucket - newidx = -1; - } - - newidx = allocFontAt(newidx); - FONSfont* font = fonts[newidx]; - font.setName(name); - font.lut.ptr[0..FONS_HASH_LUT_SIZE] = -1; // init hash lookup - font.fdata = fdata; // set the font data (don't change reference count) - fons__tt_setMono(&this, &font.font, !defAA); - - // init font - nscratch = 0; - if (!fons__tt_loadFont(&this, &font.font, fdata.data, fdata.dataSize)) { - // we promised to not free data on error, so just clear the data store (it will be freed by the caller) - font.fdata = null; - font.kill(); - if (oldidx != FONS_INVALID) { - assert(oldidx == newidx); - fonts[oldidx] = oldfont; - } else { - assert(newidx == nfonts-1); - fonts[newidx] = null; - --nfonts; - } - return FONS_INVALID; - } else { - // free old font data, if any - if (oldfont !is null) oldfont.kill(); - } - - // add font to name hash - if (oldidx == FONS_INVALID) addIndexToHash(newidx); - - // store normalized line height - // the real line height is got by multiplying the lineh by font size - fons__tt_getFontVMetrics(&font.font, &ascent, &descent, &lineGap); - fh = ascent-descent; - font.ascender = cast(float)ascent/cast(float)fh; - font.descender = cast(float)descent/cast(float)fh; - font.lineh = cast(float)(fh+lineGap)/cast(float)fh; - - //{ import core.stdc.stdio; printf("created font [%.*s] (idx=%d)...\n", cast(uint)name.length, name.ptr, idx); } - return newidx; - } - - // returns `null` on invalid index - // $(WARNING copy name, as name buffer can be invalidated by next fontstash API call!) - const(char)[] getNameByIndex (int idx) const pure nothrow @trusted @nogc { + /// Returns logical font name corresponding to the given font id, or `null`. + /// $(WARNING Copy returned name, as name buffer can be invalidated by next FontStash API call!) + const(char)[] getNameById (int idx) const pure nothrow @trusted @nogc { if (idx < 0 || idx >= nfonts || fonts[idx] is null) return null; return fonts[idx].name[0..fonts[idx].namelen]; } - // allowSubstitutes: check AA variants if exact name wasn't found? - // return [FONS_INVALID] if no font was found + /// Returns font id corresponding to the given logical font name, or [FONS_INVALID]. int getFontByName (const(char)[] name) const pure nothrow @trusted @nogc { //{ import core.stdc.stdio; printf("fonsGetFontByName: [%.*s]\n", cast(uint)name.length, name.ptr); } // remove ":noaa" suffix @@ -11191,6 +11268,10 @@ public: return findNameInHash(name); } + /** Measures the specified text string. Parameter bounds should be a float[4], + * if the bounding box of the text should be returned. The bounds value are [xmin, ymin, xmax, ymax] + * Returns the horizontal advance of the measured text (i.e. where the next character should drawn). + */ float getTextBounds(T) (float x, float y, const(T)[] str, float[] bounds) nothrow @trusted @nogc if (isAnyCharType!T) { FONSstate* state = getState; uint codepoint; @@ -11226,12 +11307,12 @@ public: } codepoint = cast(uint)ch; } - glyph = getGlyph(font, codepoint, isize, iblur, FONS_GLYPH_BITMAP_OPTIONAL); + glyph = getGlyph(font, codepoint, isize, iblur, FONSBitmapFlag.Optional); if (glyph !is null) { getQuad(font, prevGlyphIndex, glyph, isize/10.0f, scale, state.spacing, &x, &y, &q); if (q.x0 < minx) minx = q.x0; if (q.x1 > maxx) maxx = q.x1; - if (params.flags&FONS_ZERO_TOPLEFT) { + if (params.isZeroTopLeft) { if (q.y0 < miny) miny = q.y0; if (q.y1 > maxy) maxy = q.y1; } else { @@ -11269,26 +11350,29 @@ public: return advance; } + /// Returns various font metrics. Any argument can be `null` if you aren't interested in its value. void getVertMetrics (float* ascender, float* descender, float* lineh) nothrow @trusted @nogc { - FONSfont* font; FONSstate* state = getState; - short isize; - if (state.font < 0 || state.font >= nfonts) { if (ascender !is null) *ascender = 0; if (descender !is null) *descender = 0; if (lineh !is null) *lineh = 0; - return; + } else { + FONSfont* font = fonts[state.font]; + if (font is null || font.fdata is null) { + if (ascender !is null) *ascender = 0; + if (descender !is null) *descender = 0; + if (lineh !is null) *lineh = 0; + } else { + short isize = cast(short)(state.size*10.0f); + if (ascender !is null) *ascender = font.ascender*isize/10.0f; + if (descender !is null) *descender = font.descender*isize/10.0f; + if (lineh !is null) *lineh = font.lineh*isize/10.0f; + } } - font = fonts[state.font]; - isize = cast(short)(state.size*10.0f); - if (font is null || font.fdata is null) return; - - if (ascender) *ascender = font.ascender*isize/10.0f; - if (descender) *descender = font.descender*isize/10.0f; - if (lineh) *lineh = font.lineh*isize/10.0f; } + /// Returns line bounds. Any argument can be `null` if you aren't interested in its value. void getLineBounds (float y, float* minyp, float* maxyp) nothrow @trusted @nogc { FONSfont* font; FONSstate* state = getState; @@ -11304,7 +11388,7 @@ public: y += getVertAlign(font, state.talign, isize); - if (params.flags&FONS_ZERO_TOPLEFT) { + if (params.isZeroTopLeft) { immutable float miny = y-font.ascender*cast(float)isize/10.0f; immutable float maxy = miny+font.lineh*isize/10.0f; if (minyp !is null) *minyp = miny; @@ -11317,12 +11401,35 @@ public: } } + /// Returns font line height. + float fontHeight () nothrow @trusted @nogc { + float res = void; + getVertMetrics(null, null, &res); + return res; + } + + /// Returns font ascender (positive). + float fontAscender () nothrow @trusted @nogc { + float res = void; + getVertMetrics(&res, null, null); + return res; + } + + /// Returns font descender (negative). + float fontDescender () nothrow @trusted @nogc { + float res = void; + getVertMetrics(null, &res, null); + return res; + } + + //TODO: document this const(ubyte)* getTextureData (int* width, int* height) nothrow @trusted @nogc { if (width !is null) *width = params.width; if (height !is null) *height = params.height; return texData; } + //TODO: document this bool validateTexture (int* dirty) nothrow @trusted @nogc { if (dirtyRect.ptr[0] < dirtyRect.ptr[2] && dirtyRect.ptr[1] < dirtyRect.ptr[3]) { dirty[0] = dirtyRect.ptr[0]; @@ -11339,15 +11446,18 @@ public: return false; } - void errorCallback (void delegate (int error, int val) nothrow @trusted @nogc callback) nothrow @trusted @nogc { + //TODO: document this + void errorCallback (void delegate (FONSError error, int val) nothrow @trusted @nogc callback) nothrow @trusted @nogc { handleError = callback; } + //TODO: document this void getAtlasSize (int* width, int* height) const pure nothrow @trusted @nogc { if (width !is null) *width = params.width; if (height !is null) *height = params.height; } + //TODO: document this bool expandAtlas (int width, int height) nothrow @trusted @nogc { import core.stdc.stdlib : free; import core.stdc.string : memcpy, memset; @@ -11399,6 +11509,7 @@ public: return true; } + //TODO: document this bool resetAtlas (int width, int height) nothrow @trusted @nogc { import core.stdc.stdlib : realloc; import core.stdc.string : memcpy, memset; @@ -11444,6 +11555,7 @@ public: return true; } + //TODO: document this bool getPathBounds (dchar dch, float[] bounds) nothrow @trusted @nogc { if (bounds.length > 4) bounds = bounds.ptr[0..4]; static if (is(typeof(&fons__nvg__bounds))) { @@ -11460,6 +11572,7 @@ public: } } + //TODO: document this bool toPath() (NVGContext vg, dchar dch, float[] bounds=null) nothrow @trusted @nogc { if (bounds.length > 4) bounds = bounds.ptr[0..4]; static if (is(typeof(&fons__nvg__toPath))) { @@ -11477,6 +11590,7 @@ public: } } + //TODO: document this bool toOutline (dchar dch, NVGPathOutline.DataStore* ol) nothrow @trusted @nogc { if (ol is null) return false; static if (is(typeof(&fons__nvg__toOutline))) { @@ -11492,7 +11606,8 @@ public: } } - FONSglyph* getGlyph (FONSfont* font, uint codepoint, short isize, short iblur, FONSglyphBitmap bitmapOption) nothrow @trusted @nogc { + //TODO: document this + FONSglyph* getGlyph (FONSfont* font, uint codepoint, short isize, short iblur, FONSBitmapFlag bitmapOption) nothrow @trusted @nogc { static uint fons__hashint() (uint a) pure nothrow @safe @nogc { pragma(inline, true); a += ~(a<<15); @@ -11578,7 +11693,7 @@ public: if (font.glyphs[i].codepoint == codepoint && font.glyphs[i].size == isize && font.glyphs[i].blur == iblur) { glyph = &font.glyphs[i]; // Negative coordinate indicates there is no bitmap data created. - if (bitmapOption == FONS_GLYPH_BITMAP_OPTIONAL || (glyph.x0 >= 0 && glyph.y0 >= 0)) return glyph; + if (bitmapOption == FONSBitmapFlag.Optional || (glyph.x0 >= 0 && glyph.y0 >= 0)) return glyph; // At this point, glyph exists but the bitmap data is not yet created. break; } @@ -11597,12 +11712,12 @@ public: int gh = y1-y0+pad*2; // Determines the spot to draw glyph in the atlas. - if (bitmapOption == FONS_GLYPH_BITMAP_REQUIRED) { + if (bitmapOption == FONSBitmapFlag.Required) { // Find free spot for the rect in the atlas. bool added = atlas.addRect(gw, gh, &gx, &gy); if (!added && handleError !is null) { // Atlas is full, let the user to resize the atlas (or not), and try again. - handleError(FONS_ATLAS_FULL, 0); + handleError(FONSError.AtlasFull, 0); added = atlas.addRect(gw, gh, &gx, &gy); } if (!added) return null; @@ -11633,7 +11748,7 @@ public: glyph.xoff = cast(short)(x0-pad); glyph.yoff = cast(short)(y0-pad); - if (bitmapOption == FONS_GLYPH_BITMAP_OPTIONAL) return glyph; + if (bitmapOption == FONSBitmapFlag.Optional) return glyph; // Rasterize ubyte* dst = &texData[(glyph.x0+pad)+(glyph.y0+pad)*params.width]; @@ -11676,6 +11791,7 @@ public: return glyph; } + //TODO: document this void getQuad (FONSfont* font, int prevGlyphIndex, FONSglyph* glyph, float size, float scale, float spacing, float* x, float* y, FONSQuad* q) nothrow @trusted @nogc { if (prevGlyphIndex >= 0) { immutable float adv = fons__tt_getGlyphKernAdvance(&font.font, size, prevGlyphIndex, glyph.index)/**scale*/; //k8: do we really need scale here? @@ -11693,7 +11809,7 @@ public: immutable float x1 = cast(float)(glyph.x1-1); immutable float y1 = cast(float)(glyph.y1-1); - if (params.flags&FONS_ZERO_TOPLEFT) { + if (params.isZeroTopLeft) { immutable float rx = cast(float)cast(int)(*x+xoff); immutable float ry = cast(float)cast(int)(*y+yoff); @@ -11735,28 +11851,10 @@ public: dirtyRect.ptr[3] = 0; } } - - float getVertAlign (FONSfont* font, NVGTextAlign talign, short isize) nothrow @trusted @nogc { - if (params.flags&FONS_ZERO_TOPLEFT) { - final switch (talign.vertical) { - case NVGTextAlign.V.Top: return font.ascender*cast(float)isize/10.0f; - case NVGTextAlign.V.Middle: return (font.ascender+font.descender)/2.0f*cast(float)isize/10.0f; - case NVGTextAlign.V.Baseline: return 0.0f; - case NVGTextAlign.V.Bottom: return font.descender*cast(float)isize/10.0f; - } - } else { - final switch (talign.vertical) { - case NVGTextAlign.V.Top: return -font.ascender*cast(float)isize/10.0f; - case NVGTextAlign.V.Middle: return -(font.ascender+font.descender)/2.0f*cast(float)isize/10.0f; - case NVGTextAlign.V.Baseline: return 0.0f; - case NVGTextAlign.V.Bottom: return -font.descender*cast(float)isize/10.0f; - } - } - assert(0); - } } -// +/// Free all resources used by the `stash`, and `stash` itself. +/// Group: font_stash public void kill (ref FONSContext stash) nothrow @trusted @nogc { import core.stdc.stdlib : free; if (stash is null) return; @@ -11773,7 +11871,7 @@ void* fons__tmpalloc (usize size, void* up) nothrow @trusted @nogc { // 16-byte align the returned pointer size = (size+0xf)&~0xf; if (stash.nscratch+cast(int)size > FONS_SCRATCH_BUF_SIZE) { - if (stash.handleError !is null) stash.handleError(FONS_SCRATCH_FULL, stash.nscratch+cast(int)size); + if (stash.handleError !is null) stash.handleError(FONSError.ScratchFull, stash.nscratch+cast(int)size); return null; } ptr = stash.scratch+stash.nscratch; @@ -11836,42 +11934,51 @@ uint fons__decutf8 (uint* state, uint* codep, uint byte_) { // ////////////////////////////////////////////////////////////////////////// // -public struct FonsTextBoundsIterator { +/// This iterator can be used to do text measurement. +/// $(WARNING Don't add new fonts to stash while you are iterating, or you WILL get segfault!) +/// Group: font_stash +public struct FONSTextBoundsIterator { private: FONSContext stash; - FONSstate* state; - uint codepoint; + FONSstate state; + uint codepoint = 0xFFFD; uint utf8state = 0; - FONSQuad q; - FONSglyph* glyph = null; int prevGlyphIndex = -1; short isize, iblur; - float scale; + float scale = 0; FONSfont* font; - float startx, x, y; - float minx, miny, maxx, maxy; + float startx = 0, x = 0, y = 0; + float minx = 0, miny = 0, maxx = 0, maxy = 0; + +private: + void clear () nothrow @trusted @nogc { + import core.stdc.string : memset; + memset(&this, 0, this.sizeof); + this.prevGlyphIndex = -1; + this.codepoint = 0xFFFD; + } public: - this (FONSContext astash, float ax, float ay) nothrow @trusted @nogc { reset(astash, ax, ay); } + /// Initialize iterator with the current FontStash state. FontStash state can be changed after initialization without affecting the iterator. + this (FONSContext astash, float ax=0, float ay=0) nothrow @trusted @nogc { reset(astash, ax, ay); } - void reset (FONSContext astash, float ax, float ay) nothrow @trusted @nogc { - this = this.init; - if (astash is null) return; + /// (Re)initialize iterator with the current FontStash state. FontStash state can be changed after initialization without affecting the iterator. + void reset (FONSContext astash, float ax=0, float ay=0) nothrow @trusted @nogc { + clear(); + + if (astash is null || astash.nstates == 0) return; stash = astash; - state = stash.getState; - if (state is null) { stash = null; return; } // alas + state = *stash.getState; + + if (state.font < 0 || state.font >= stash.nfonts) { clear(); return; } + font = stash.fonts[state.font]; + if (font is null || font.fdata is null) { clear(); return; } x = ax; y = ay; - isize = cast(short)(state.size*10.0f); iblur = cast(short)state.blur; - - if (state.font < 0 || state.font >= stash.nfonts) { stash = null; return; } - font = stash.fonts[state.font]; - if (font is null || font.fdata is null) { stash = null; return; } - scale = fons__tt_getPixelHeightScale(&font.font, cast(float)isize/10.0f); // align vertically @@ -11880,20 +11987,20 @@ public: minx = maxx = x; miny = maxy = y; startx = x; - //assert(prevGlyphIndex == -1); } -public: - @property bool valid () const pure nothrow @safe @nogc { pragma(inline, true); return (state !is null); } + /// Can this iterator be used? + @property bool valid () const pure nothrow @safe @nogc { pragma(inline, true); return (stash !is null); } + /// Put some text into iterator, calculate new values. void put(T) (const(T)[] str...) nothrow @trusted @nogc if (isAnyCharType!T) { enum DoCodePointMixin = q{ - glyph = stash.getGlyph(font, codepoint, isize, iblur, FONS_GLYPH_BITMAP_OPTIONAL); + glyph = stash.getGlyph(font, codepoint, isize, iblur, FONSBitmapFlag.Optional); if (glyph !is null) { stash.getQuad(font, prevGlyphIndex, glyph, isize/10.0f, scale, state.spacing, &x, &y, &q); if (q.x0 < minx) minx = q.x0; if (q.x1 > maxx) maxx = q.x1; - if (stash.params.flags&FONS_ZERO_TOPLEFT) { + if (stash.params.isZeroTopLeft) { if (q.y0 < miny) miny = q.y0; if (q.y1 > maxy) maxy = q.y1; } else { @@ -11906,7 +12013,11 @@ public: } }; - if (state is null) return; // alas + if (stash is null || str.length == 0) return; // alas + + FONSQuad q; + FONSglyph* glyph; + static if (is(T == char)) { foreach (char ch; str) { mixin(DecUtfMixin!("utf8state", "codepoint", "cast(ubyte)ch")); @@ -11914,7 +12025,6 @@ public: mixin(DoCodePointMixin); } } else { - if (str.length == 0) return; if (utf8state) { utf8state = 0; codepoint = 0xFFFD; @@ -11930,11 +12040,12 @@ public: } } - // return current advance - @property float advance () const pure nothrow @safe @nogc { pragma(inline, true); return (state !is null ? x-startx : 0); } + /// Returns current advance. + @property float advance () const pure nothrow @safe @nogc { pragma(inline, true); return (stash !is null ? x-startx : 0); } + /// Returns current text bounds. void getBounds (ref float[4] bounds) const pure nothrow @safe @nogc { - if (state is null) { bounds[] = 0; return; } + if (stash is null) { bounds[] = 0; return; } float lminx = minx, lmaxx = maxx; // align horizontally if (state.talign.left) { @@ -11954,9 +12065,9 @@ public: bounds[3] = maxy; } - // Returns current horizontal text bounds. + /// Returns current horizontal text bounds. void getHBounds (out float xmin, out float xmax) nothrow @trusted @nogc { - if (state !is null) { + if (stash !is null) { float lminx = minx, lmaxx = maxx; // align horizontally if (state.talign.left) { @@ -11972,16 +12083,39 @@ public: } xmin = lminx; xmax = lmaxx; + } else { + xmin = xmax = 0; } } - // Returns current vertical text bounds. + /// Returns current vertical text bounds. void getVBounds (out float ymin, out float ymax) nothrow @trusted @nogc { - if (state !is null) { + pragma(inline, true); + if (stash !is null) { ymin = miny; ymax = maxy; + } else { + ymin = ymax = 0; } } + + /// Returns font line height. + float lineHeight () nothrow @trusted @nogc { + pragma(inline, true); + return (stash !is null ? stash.fonts[state.font].lineh*cast(short)(state.size*10.0f)/10.0f : 0); + } + + /// Returns font ascender (positive). + float ascender () nothrow @trusted @nogc { + pragma(inline, true); + return (stash !is null ? stash.fonts[state.font].ascender*cast(short)(state.size*10.0f)/10.0f : 0); + } + + /// Returns font descender (negative). + float descender () nothrow @trusted @nogc { + pragma(inline, true); + return (stash !is null ? stash.fonts[state.font].descender*cast(short)(state.size*10.0f)/10.0f : 0); + } }