2 * Simple Framebuffer Gfx/GUI lib
4 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
5 * Understanding is not required. Only obedience.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, version 3 of the License ONLY.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 module iv
.egra
.gfx
.text
/*is aliced*/;
30 import iv
.egra
.gfx
.config
;
31 import iv
.egra
.gfx
.base
;
34 // ////////////////////////////////////////////////////////////////////////// //
35 enum ReplacementChar
= 0xFFFD;
36 //__gshared string chiFontName = "Arial:pixelsize=16";
37 //__gshared string chiFontName = "Verdana:pixelsize=16";
38 __gshared string chiFontName
= "Verdana:weight=bold:pixelsize=16";
39 __gshared string chiFontFile
;
40 __gshared
int fontSize
;
41 __gshared
int fontHeight
;
42 __gshared
int fontBaselineOfs
;
45 // ////////////////////////////////////////////////////////////////////////// //
46 __gshared
ubyte* ttfontdata
;
47 __gshared
uint ttfontdatasize
;
49 __gshared FT_Library ttflibrary
;
50 __gshared FTC_Manager ttfcache
;
51 __gshared FTC_CMapCache ttfcachecmap
;
52 __gshared FTC_ImageCache ttfcacheimage
;
55 shared static ~this () {
58 FTC_Manager_Done(ttfcache
);
61 FT_Done_FreeType(ttflibrary
);
67 enum FontID
= cast(FTC_FaceID
)1;
71 void ttfFontFinalizer (void* obj
) {
72 import core
.stdc
.stdlib
: free
;
73 if (obj
is null) return;
74 auto tf
= cast(iv
.freetype
.FT_Face
)obj
;
75 if (tf
.generic
.data
!is ttfontdata
) return;
76 if (ttfontdata
!is null) {
77 version(aliced
) conwriteln("TTF CACHE: freeing loaded font...");
84 FT_Error
ttfFontLoader (FTC_FaceID face_id
, FT_Library library
, FT_Pointer request_data
, iv
.freetype
.FT_Face
* aface
) {
85 if (face_id
== FontID
) {
87 if (ttfontdata
is null) {
88 conwriteln("TTF CACHE: loading '", chiFontFile
, "'...");
89 import core
.stdc
.stdlib
: malloc
;
90 auto fl
= VFile(chiFontFile
);
92 if (fsz
< 16 || fsz
> int.max
/8) throw new Exception("invalid ttf size");
93 ttfontdatasize
= cast(uint)fsz
;
94 ttfontdata
= cast(ubyte*)malloc(ttfontdatasize
);
95 if (ttfontdata
is null) { import core
.exception
: onOutOfMemoryErrorNoGC
; onOutOfMemoryErrorNoGC(); }
96 fl
.rawReadExact(ttfontdata
[0..ttfontdatasize
]);
98 auto res
= FT_New_Memory_Face(library
, cast(const(FT_Byte
)*)ttfontdata
, ttfontdatasize
, 0, aface
);
99 if (res
!= 0) throw new Exception("error loading ttf: '"~chiFontFile
~"'");
100 (*aface
).generic
.data
= ttfontdata
;
101 (*aface
).generic
.finalizer
= &ttfFontFinalizer
;
102 } catch (Exception e
) {
103 if (ttfontdata
!is null) {
104 import core
.stdc
.stdlib
: free
;
109 version(aliced
) conwriteln("ERROR loading font: ", e
.msg
);
110 return FT_Err_Cannot_Open_Resource
;
114 version(aliced
) conwriteln("TTF CACHE: invalid font id");
116 return FT_Err_Cannot_Open_Resource
;
121 void ttfLoad () nothrow {
122 if (FT_Init_FreeType(&ttflibrary
)) assert(0, "can't initialize FreeType");
123 if (FTC_Manager_New(ttflibrary
, 0, 0, 0, &ttfFontLoader
, null, &ttfcache
)) assert(0, "can't initialize FreeType cache manager");
124 if (FTC_CMapCache_New(ttfcache
, &ttfcachecmap
)) assert(0, "can't initialize FreeType cache manager");
125 if (FTC_ImageCache_New(ttfcache
, &ttfcacheimage
)) assert(0, "can't initialize FreeType cache manager");
128 fsc
.face_id
= FontID
;
130 fsc
.height
= fontSize
;
131 fsc
.pixel
= 1; // size in pixels
134 if (FTC_Manager_LookupSize(ttfcache
, &fsc
, &ttfontsz
)) assert(0, "cannot find FreeType font");
135 fontHeight
= cast(int)ttfontsz
.metrics
.height
>>6; // 26.6
136 fontBaselineOfs
= cast(int)((ttfontsz
.metrics
.height
+ttfontsz
.metrics
.descender
)>>6);
137 if (fontHeight
< 2 || fontHeight
> 128) assert(0, "invalid FreeType font metrics");
139 version(aliced
) conwriteln("TTF CACHE initialized.");
143 void initFontEngine () nothrow {
144 if (ttflibrary
is null) {
145 import std
.string
: fromStringz
, toStringz
;
146 if (!FcInit()) assert(0, "cannot init fontconfig");
147 iv
.fontconfig
.FcPattern
* pat
= FcNameParse(chiFontName
.toStringz
);
148 if (pat
is null) assert(0, "cannot parse font name");
149 if (!FcConfigSubstitute(null, pat
, FcMatchPattern
)) assert(0, "cannot find fontconfig substitute");
150 FcDefaultSubstitute(pat
);
152 iv
.fontconfig
.FcResult result
;
153 iv
.fontconfig
.FcPattern
* font
= FcFontMatch(null, pat
, &result
);
156 if (FcPatternGetString(font
, FC_FILE
, 0, &file
) == FcResultMatch
) {
157 version(aliced
) conwriteln("font file: [", file
, "]");
158 chiFontFile
= file
.fromStringz
.idup
;
161 if (FcPatternGetDouble(font
, FC_PIXEL_SIZE
, 0, &pixelsize
) == FcResultMatch
) {
162 version(aliced
) conwriteln("pixel size: ", pixelsize
);
163 fontSize
= cast(int)pixelsize
;
166 FcPatternDestroy(pat
);
168 if (fontSize
< 6) fontSize
= 6;
169 if (fontSize
> 42) fontSize
= 42;
175 // ////////////////////////////////////////////////////////////////////////// //
176 public void utfByDChar (const(char)[] s
, scope void delegate (dchar ch
) nothrow @trusted dg
) nothrow @trusted {
177 if (dg
is null) return;
179 foreach (char ch
; s
) {
180 if (dc
.decode(cast(ubyte)ch
)) dg(dc
.complete ? dc
.codepoint
: dc
.replacement
);
185 public void utfByDCharSPos (const(char)[] s
, scope void delegate (dchar ch
, usize stpos
) nothrow @trusted dg
) nothrow @trusted {
186 if (dg
is null) return;
189 foreach (immutable idx
, char ch
; s
) {
190 if (dc
.decode(cast(ubyte)ch
)) {
191 dg(dc
.complete ? dc
.codepoint
: dc
.replacement
, stpos
);
198 // ////////////////////////////////////////////////////////////////////////// //
199 void drawFTBitmap (int x
, int y
, in ref FT_Bitmap bitmap
, uint clr
) nothrow @trusted @nogc {
200 if (bitmap
.pixel_mode
!= FT_PIXEL_MODE_MONO
) return; // alas
201 if (bitmap
.rows
< 1 || bitmap
.width
< 1) return; // nothing to do
202 if (gxIsTransparent(clr
)) return; // just in case
205 const(ubyte)* src
= bitmap
.buffer
;
206 int spt
= bitmap
.pitch
;
210 src
+= spt
*(bitmap
.rows
-1);
212 if (!gxIsSolid(clr
)) {
214 foreach (immutable int dy
; 0..bitmap
.rows
) {
215 ubyte count
= 0, b
= 0;
217 foreach (immutable int dx
; 0..bitmap
.width
) {
218 if (count
-- == 0) { count
= 7; b
= *ss
++; } else b
<<= 1;
219 if (b
&0x80) gxPutPixel(x
+dx
, y
, clr
);
222 if (topdown
) src
+= spt
; else src
-= spt
;
226 // check if we can use the fastest path
227 auto brc
= GxRect(x
, y
, bitmap
.width
, bitmap
.rows
);
228 if (gxClipRect
.contains(brc
) && GxRect(0, 0, VBufWidth
, VBufHeight
).contains(brc
)) {
229 // yay, the fastest one!
230 uint* dptr
= vglTexBuf
+y
*VBufWidth
+x
;
231 foreach (immutable int dy
; 0..bitmap
.rows
) {
232 ubyte count
= 0, b
= 0;
235 foreach (immutable int dx
; 0..bitmap
.width
) {
236 if (count
-- == 0) { count
= 7; b
= *ss
++; } else b
<<= 1;
237 if (b
&0x80) *curptr
= clr
;
240 if (topdown
) src
+= spt
; else src
-= spt
;
245 foreach (immutable int dy
; 0..bitmap
.rows
) {
246 ubyte count
= 0, b
= 0;
248 foreach (immutable int dx
; 0..bitmap
.width
) {
249 if (count
-- == 0) { count
= 7; b
= *ss
++; } else b
<<= 1;
250 if (b
&0x80) gxSetPixel(x
+dx
, y
, clr
);
253 if (topdown
) src
+= spt
; else src
-= spt
;
259 // y is baseline; returns advance
260 int ttfDrawGlyph (bool firstchar
, int scale
, int x
, int y
, int glyphidx
, uint clr
) nothrow @trusted {
262 if (glyphidx
== 0) return 0;
264 FTC_ImageTypeRec fimg
;
265 fimg
.face_id
= FontID
;
267 fimg
.height
= fontSize
*scale
;
269 fimg
.flags
= FT_LOAD_TARGET_MONO|
(gxIsTransparent(clr
) ?
0 : FT_LOAD_MONOCHROME|FT_LOAD_RENDER
);
271 fimg
.flags
= (gxIsTransparent(clr
) ?
0 : FT_LOAD_RENDER
);
275 if (FTC_ImageCache_Lookup(ttfcacheimage
, &fimg
, glyphidx
, &fg
, null)) return 0;
278 if (!gxIsTransparent(clr
)) {
279 if (fg
.format
!= FT_GLYPH_FORMAT_BITMAP
) return 0;
280 FT_BitmapGlyph fgi
= cast(FT_BitmapGlyph
)fg
;
282 if (firstchar
&& fgi
.bitmap
.width
> 0) { x0
-= fgi
.left
; advdec
= fgi
.left
; }
283 drawFTBitmap(x0
, y
-fgi
.top
, fgi
.bitmap
, clr
);
285 return cast(int)(fg
.advance
.x
>>16)-advdec
;
289 int ttfGetKerning (int scale
, int gl0idx
, int gl1idx
) nothrow @trusted {
290 if (gl0idx
== 0 || gl1idx
== 0) return 0;
293 fsc
.face_id
= FontID
;
295 fsc
.height
= fontSize
*scale
;
296 fsc
.pixel
= 1; // size in pixels
299 if (FTC_Manager_LookupSize(ttfcache
, &fsc
, &ttfontsz
)) return 0;
300 if (!FT_HAS_KERNING(ttfontsz
.face
)) return 0;
303 if (FT_Get_Kerning(ttfontsz
.face
, gl0idx
, gl1idx
, FT_KERNING_UNSCALED
, &kk
)) return 0;
305 auto kadvfrac
= FT_MulFix(kk
.x
, ttfontsz
.metrics
.x_scale
); // 1/64 of pixel
306 return cast(int)((kadvfrac
/*+(kadvfrac < 0 ? -32 : 32)*/)>>6);
310 // ////////////////////////////////////////////////////////////////////////// //
311 public int gxCharWidthScaled (int scale
, dchar ch
) nothrow @trusted {
312 if (scale
< 1) return 0;
316 int glyph
= FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, ch
);
317 if (glyph
== 0) glyph
= FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, ReplacementChar
);
318 if (glyph
== 0) return 0;
320 FTC_ImageTypeRec fimg
;
321 fimg
.face_id
= FontID
;
323 fimg
.height
= fontSize
*scale
;
324 fimg
.flags
= FT_LOAD_TARGET_MONO
;
327 if (FTC_ImageCache_Lookup(ttfcacheimage
, &fimg
, glyph
, &fg
, null)) return -666;
329 int res
= cast(int)fg
.advance
.x
>>16;
330 return (res
> 0 ? res
: 0);
333 public int gxCharWidth (dchar ch
) nothrow @trusted { return gxCharWidthScaled(1, ch
); }
336 public int gxDrawChar (int x
, int y
, dchar ch
, uint fg
, int prevcp
=-1) nothrow @trusted { return gxDrawCharScaled(1, x
, y
, ch
, fg
, prevcp
); }
337 public int gxDrawChar() (in auto ref GxPoint p
, char ch
, uint fg
, int prevcp
=-1) nothrow @trusted { return gxDrawCharScaled(1, p
.x
, p
.y
, ch
, fg
, prevcp
); }
340 // ////////////////////////////////////////////////////////////////////////// //
342 public int gxDrawCharScaled (int scale
, int x
, int y
, dchar ch
, uint clr
, int prevcp
=-1) nothrow @trusted {
343 if (scale
< 1) return 0;
345 int glyph
= FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, ch
);
346 if (glyph
== 0) glyph
= FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, ReplacementChar
);
347 if (glyph
== 0) return 0;
349 int kadv
= ttfGetKerning(scale
, (prevcp
>= 0 ?
FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, prevcp
) : 0), glyph
);
350 return ttfDrawGlyph(false, scale
, x
+kadv
, y
+fontBaselineOfs
*scale
, glyph
, clr
);
353 public int gxDrawCharScaled() (int scale
, in auto ref GxPoint p
, char ch
, uint fg
, int prevcp
=-1) nothrow @trusted { return gxDrawCharScaled(scale
, p
.x
, p
.y
, ch
, fg
, prevcp
); }
356 // ////////////////////////////////////////////////////////////////////////// //
357 public struct GxKerning
{
364 bool firstchar
= true;
367 this (int atabsize
, int ascale
=1, bool firstCharIsFull
=false) {
369 firstchar
= !firstCharIsFull
;
371 if ((tabsize
= (atabsize
> 0 ? atabsize
: 0)) != 0) tabsize
= tabsize
*gxCharWidthScaled(ascale
, ' ');
374 void reset (int atabsize
) {
380 if ((tabsize
= (atabsize
> 0 ? atabsize
: 0)) != 0) tabsize
= tabsize
*gxCharWidthScaled(scale
, ' ');
384 // tab length for current position
385 int tablength () { pragma(inline
, true); return (tabsize
> 0 ?
(wdt
/tabsize
+1)*tabsize
-wdt
: 0); }
387 int fixWidthPre (dchar ch
) {
388 immutable int prevgl
= prevgidx
;
393 if (ch
== '\t' && tabsize
> 0) {
395 lastadv
= lastcw
= tablength
;
399 int glyph
= FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, ch
);
400 if (glyph
== 0) glyph
= FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, ReplacementChar
);
402 wdt
+= ttfGetKerning(scale
, prevgl
, glyph
);
404 FTC_ImageTypeRec fimg
;
405 fimg
.face_id
= FontID
;
407 fimg
.height
= fontSize
*scale
;
409 fimg
.flags
= FT_LOAD_TARGET_MONO
;
411 fimg
.flags
= FT_LOAD_TARGET_MONO|FT_LOAD_MONOCHROME|FT_LOAD_RENDER
;
416 if (FTC_ImageCache_Lookup(ttfcacheimage
, &fimg
, glyph
, &fg
, null) == 0) {
418 lastadv
= fg
.advance
.x
>>16;
421 if (FTC_ImageCache_Lookup(ttfcacheimage
, &fimg
, glyph
, &fg
, null) == 0) {
423 if (fg
.format
== FT_GLYPH_FORMAT_BITMAP
) {
424 FT_BitmapGlyph fgi
= cast(FT_BitmapGlyph
)fg
;
425 if (firstchar
&& fgi
.bitmap
.width
> 0) {
426 lastcw
= fgi
.bitmap
.width
;
428 if (lastcw
< 1) { advdec
= 0; lastcw
= cast(int)fg
.advance
.x
>>16; }
430 lastcw
= fgi
.left
+fgi
.bitmap
.width
;
431 if (lastcw
< 1) lastcw
= cast(int)fg
.advance
.x
>>16;
435 lastadv
= (cast(int)fg
.advance
.x
>>16)-advdec
;
444 @property int finalWidth () const { pragma(inline
, true); return wdt+/
*lastadv*/lastcw
; }
447 @property int nextCharOfs () const { pragma(inline
, true); return wdt
+lastadv
; }
449 @property int currOfs () const { pragma(inline
, true); return wdt
; }
451 @property int nextOfsNoSpacing () const { pragma(inline
, true); return wdt
+lastcw
; }
455 // ////////////////////////////////////////////////////////////////////////// //
456 public struct GxDrawTextOptions
{
458 uint clr
= gxTransparent
;
460 bool firstCharIsFull
= false;
462 static pure nothrow @safe @nogc:
463 auto Color (uint aclr
) { pragma(inline
, true); return GxDrawTextOptions(0, aclr
, 1, false); }
464 auto Tab (int atabsize
) { pragma(inline
, true); return GxDrawTextOptions(atabsize
, gxTransparent
, 1, false); }
465 auto TabColor (int atabsize
, uint aclr
) { pragma(inline
, true); return GxDrawTextOptions(atabsize
, aclr
, 1, false); }
466 auto TabColorFirstFull (int atabsize
, uint aclr
, bool fcf
) { pragma(inline
, true); return GxDrawTextOptions(atabsize
, aclr
, 1, fcf
); }
467 auto ScaleTabColor (int ascale
, int atabsize
, uint aclr
) { pragma(inline
, true); return GxDrawTextOptions(atabsize
, aclr
, ascale
, false); }
468 auto ColorNFC (uint aclr
) { pragma(inline
, true); return GxDrawTextOptions(0, aclr
, 1, true); }
469 auto TabColorNFC (int atabsize
, uint aclr
) { pragma(inline
, true); return GxDrawTextOptions(atabsize
, aclr
, 1, true); }
470 auto ScaleTabColorNFC (int ascale
, int atabsize
, uint aclr
) { pragma(inline
, true); return GxDrawTextOptions(atabsize
, aclr
, ascale
, true); }
474 public struct GxDrawTextState
{
475 usize spos
; // current codepoint starting position
476 usize epos
; // current codepoint ending position (exclusive; i.e. *after* codepoint)
477 int curx
; // current x (before drawing the glyph)
481 // delegate should return color
482 public int gxDrawTextUtf(R
) (in auto ref GxDrawTextOptions opt
, int x
, int y
, auto ref R srng
, uint delegate (in ref GxDrawTextState state
) nothrow @trusted clrdg
=null) nothrow @trusted
483 if (Imp
!"std.range.primitives".isInputRange
!R
&& is(Imp
!"std.range.primitives".ElementEncodingType
!R
== char))
485 // rely on the assumption that font face won't be unloaded while we are in this function
486 if (opt
.scale
< 1) return 0;
490 GxDrawTextState state
;
492 y
+= fontBaselineOfs
*opt
.scale
;
494 immutable int tabpix
= (opt
.tabsize
> 0 ? opt
.tabsize
*gxCharWidthScaled(opt
.scale
, ' ') : 0);
499 immutable int stx
= x
;
502 bool firstchar
= !opt
.firstCharIsFull
;
505 while (!srng
.empty
) {
506 immutable ubyte srbyte
= cast(ubyte)srng
.front
;
510 if (dc
.decode(srbyte
)) {
511 int ch
= (dc
.complete ? dc
.codepoint
: dc
.replacement
);
516 if (opt
.tabsize
> 0) {
521 x
+= (wdt
/tabpix
+1)*tabpix
-wdt
;
522 if (clrdg
!is null) clrdg(state
);
523 state
.spos
= state
.epos
;
528 int glyph
= FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, ch
);
529 if (glyph
== 0) glyph
= FTC_CMapCache_Lookup(ttfcachecmap
, FontID
, -1, ReplacementChar
);
533 if (pgl
!= 0 && dokern
) {
534 if (ttfontsz
is null) {
536 fsc
.face_id
= FontID
;
538 fsc
.height
= fontSize
*opt
.scale
;
539 fsc
.pixel
= 1; // size in pixels
540 if (FTC_Manager_LookupSize(ttfcache
, &fsc
, &ttfontsz
)) {
544 dokern
= (FT_HAS_KERNING(ttfontsz
.face
) != 0);
549 if (FT_Get_Kerning(ttfontsz
.face
, pgl
, glyph
, FT_KERNING_UNSCALED
, &kk
) == 0) {
551 auto kadvfrac
= FT_MulFix(kk
.x
, ttfontsz
.metrics
.x_scale
); // 1/64 of pixel
552 kadv
= cast(int)((kadvfrac
/*+(kadvfrac < 0 ? -32 : 32)*/)>>6);
559 if (clrdg
!is null) { state
.curx
= x
; clr
= clrdg(state
); }
560 x
+= ttfDrawGlyph(firstchar
, opt
.scale
, x
, y
, glyph
, clr
);
563 state
.spos
= state
.epos
;
572 public int gxDrawTextUtf() (in auto ref GxDrawTextOptions opt
, int x
, int y
, const(char)[] s
, uint delegate (in ref GxDrawTextState state
) nothrow @trusted clrdg
=null) nothrow @trusted {
573 static struct StrIterator
{
576 public pure nothrow @trusted @nogc:
577 this (const(char)[] s
) { pragma(inline
, true); str = s
; }
578 @property bool empty () const { pragma(inline
, true); return (str.length
== 0); }
579 @property char front () const { pragma(inline
, true); return (str.length ?
str.ptr
[0] : 0); }
580 void popFront () { if (str.length
) str = str[1..$]; }
583 return gxDrawTextUtf(opt
, x
, y
, StrIterator(s
), clrdg
);
587 public int gxTextWidthScaledUtf (int scale
, const(char)[] s
, int tabsize
=0, bool firstCharIsFull
=false) nothrow @trusted {
588 if (scale
< 1) return 0;
589 auto kern
= GxKerning(tabsize
, scale
, firstCharIsFull
);
590 s
.utfByDChar(delegate (dchar ch
) @trusted { kern
.fixWidthPre(ch
); });
591 return kern
.finalWidth
;
596 // ////////////////////////////////////////////////////////////////////////// //
597 public int gxTextHeightUtf () nothrow @trusted { initFontEngine(); return fontHeight
; }
598 public int gxTextHeightScaledUtf (int scale
) nothrow @trusted { initFontEngine(); return (scale
< 1 ?
0 : fontHeight
*scale
); }
600 public int gxTextBaseLineUtf () nothrow @trusted { initFontEngine(); return fontBaselineOfs
; }
601 public int gxTextUnderLineUtf () nothrow @trusted { initFontEngine(); return fontBaselineOfs
+2; }
603 public int gxTextWidthUtf (const(char)[] s
, int tabsize
=0, bool firstCharIsFull
=false) nothrow @trusted { return gxTextWidthScaledUtf(1, s
, tabsize
, firstCharIsFull
); }
605 public int gxDrawTextUtf() (int x
, int y
, const(char)[] s
, uint clr
) nothrow @trusted { return gxDrawTextUtf(GxDrawTextOptions
.Color(clr
), x
, y
, s
); }
606 public int gxDrawTextUtf() (in auto ref GxPoint p
, const(char)[] s
, uint clr
) nothrow @trusted { return gxDrawTextUtf(p
.x
, p
.y
, s
, clr
); }
609 public int gxDrawTextOutScaledUtf (int scale
, int x
, int y
, const(char)[] s
, uint clr
, uint clrout
) nothrow @trusted {
610 if (scale
< 1) return 0;
611 auto opt
= GxDrawTextOptions
.ScaleTabColor(scale
, 0, clrout
);
612 foreach (immutable dy
; -1*scale
..2*scale
) {
613 foreach (immutable dx
; -1*scale
..2*scale
) {
614 if (dx || dy
) gxDrawTextUtf(opt
, x
+dx
, y
+dy
, s
);
618 return gxDrawTextUtf(opt
, x
, y
, s
);