1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23 import iv
.nanovg
.oui
.blendish
;
30 version(laytest
) import iv
.encoding
;
31 version(aliced
) {} else private alias usize
= size_t
;
33 private int lrintf (float f
) { pragma(inline
, true); return cast(int)(f
+0.5); }
35 import core
.stdc
.math
: lrintf
;
38 // ////////////////////////////////////////////////////////////////////////// //
39 abstract class LayObject
{
40 abstract int width ();
41 abstract int spacewidth ();
42 abstract int height ();
43 abstract int ascent (); // should be positive
44 abstract int descent (); // should be negative
45 abstract bool canbreak ();
46 abstract bool spaced ();
48 abstract void draw (NVGContext ctx
, float x
, float y
);
52 // ////////////////////////////////////////////////////////////////////////// //
53 // this needs such fonts in stash:
57 // textz -- italic and bold
61 // monoz -- italic and bold
62 final class LayFontStash
{
65 // list of known font faces, should be filled by caller when this object created
67 int[string
] fontfaces
;
68 string
[int] fontfaceids
;
72 bool fontWasSet
; // to ensure that first call to `setFont()` will do it's work
73 LayFontStyle lastStyle
;
77 // create new fontstash
78 FONSparams fontParams
;
79 fontParams
.width
= 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
80 fontParams
.height
= 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
81 fontParams
.flags
= FONS_ZERO_TOPLEFT
;
82 fs
= fonsCreateInternal(&fontParams
);
83 if (fs
is null) throw new Exception("error creating font stash");
85 fs
.fonsResetAtlas(1024, 1024);
86 fonsSetSpacing(fs
, 0);
88 fonsSetAlign(fs
, NVGTextAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Baseline
));
91 ~this () { freeFontStash(); }
93 void freeFontStash () {
94 if (killFontStash
&& fs
!is null) {
95 fs
.fonsDeleteInternal();
97 killFontStash
= false;
101 void addFont(T
: const(char)[], TP
: const(char)[]) (T name
, TP path
) {
102 static if (is(T
== typeof(null))) {
103 throw new Exception("invalid font face name");
105 if (name
.length
== 0) throw new Exception("invalid font face name");
106 if (name
in fontfaces
) throw new Exception("duplicate font '"~name
.idup
~"'");
107 int fid
= fs
.fonsAddFont(name
, path
);
108 if (fid
< 0) throw new Exception("font '"~name
~"' is not found at '"~path
.idup
~"'");
109 static if (is(T
== string
)) {
110 fontfaces
[name
] = fid
;
111 fontfaceids
[fid
] = name
;
113 string n
= name
.idup
;
115 fontfaceids
[fid
] = n
;
120 // returns "font id" which can be used in `fontFace()`
121 @property int fontFaceId (const(char)[] name
) {
122 if (auto fid
= name
in fontfaces
) return *fid
;
126 @property string
fontFace (int fid
) {
127 if (auto ff
= fid
in fontfaceids
) return *ff
;
131 void setFont() (in auto ref LayFontStyle style
) {
132 int fsz
= style
.fontsize
;
133 if (fsz
< 1) fsz
= 1;
134 if (!fontWasSet || fsz
!= lastStyle
.fontsize || style
.fontface
!= lastStyle
.fontface
) {
135 if (style
.fontface
!= lastStyle
.fontface
) fonsSetFont(fs
, style
.fontface
);
136 if (fsz
!= lastStyle
.fontsize
) fonsSetSize(fs
, fsz
);
138 lastStyle
.fontsize
= fsz
;
142 int textWidth(T
) (const(T
)[] str) if (is(T
== char) ||
is(T
== dchar)) {
143 import std
.algorithm
: max
;
145 float adv
= fs
.fonsTextBounds(0, 0, str, b
[]);
147 return lrintf(max(adv
, w
));
150 int spacesWidth (int count
) {
151 if (count
< 1) return 0;
152 auto it
= FonsTextBoundsIterator(fs
, 0, 0);
154 return lrintf(it
.advance
*count
);
157 // this returns "width", "width with trailing whitespace", and "width with trailing hypen"
158 // all `*` args can be omited
159 void textWidth2(T
) (const(T
)[] str, int* w
=null, int* wsp
=null, int* whyph
=null) if (is(T
== char) ||
is(T
== dchar)) {
160 import std
.algorithm
: max
;
161 if (w
is null && wsp
is null && whyph
is null) return;
163 auto it
= FonsTextBoundsIterator(fs
, 0, 0);
166 it
.getHBounds(minx
, maxx
);
167 *w
= lrintf(max(it
.advance
, maxx
-minx
));
169 if (wsp
!is null && whyph
is null) {
171 it
.getHBounds(minx
, maxx
);
172 *wsp
= lrintf(max(it
.advance
, maxx
-minx
));
173 } else if (wsp
is null && whyph
!is null) {
174 it
.put(cast(dchar)45);
175 it
.getHBounds(minx
, maxx
);
176 *whyph
= lrintf(max(it
.advance
, maxx
-minx
));
177 } else if (wsp
!is null && whyph
!is null) {
180 it
.getHBounds(minx
, maxx
);
181 *wsp
= lrintf(max(it
.advance
, maxx
-minx
));
182 sit
.put(cast(dchar)45);
183 sit
.getHBounds(minx
, maxx
);
184 *whyph
= lrintf(max(sit
.advance
, maxx
-minx
));
189 // use line bounds for height
190 float y0
= void, y1
= void;
191 fs
.fonsLineBounds(0, &y0
, &y1
);
192 return lrintf(y1
-y0
);
195 void textMetrics (int* asc
, int* desc
, int* lineh
) {
196 float a
= void, d
= void, h
= void;
197 fs
.fonsVertMetrics(&a
, &d
, &h
);
198 if (asc
!is null) *asc
= lrintf(a
);
199 if (desc
!is null) *desc
= lrintf(d
);
200 if (lineh
!is null) *lineh
= lrintf(h
);
205 // ////////////////////////////////////////////////////////////////////////// //
206 // generic text style
207 align(1) struct LayFontStyle
{
215 Href
= 1<<5, // this is cross-reference (not actually a style flag, but it somewhat fits here)
217 ubyte flags
; // see above
218 int fontface
= -1; // i can't use strings here, as this struct inside LayWord will not be GC-scanned
220 uint color
= 0xff000000; // AABBGGRR; AA usually ignored by renderer, but i'll keep it anyway
221 string
toString () const {
222 import std
.format
: format
;
223 string res
= "font:%s;size:%s;color:0x%08X".format(fontface
, fontsize
, color
);
224 if (flags
&Flag
.Italic
) res
~= ";italic";
225 if (flags
&Flag
.Bold
) res
~= ";bold";
226 if (flags
&Flag
.Strike
) res
~= ";strike";
227 if (flags
&Flag
.Underline
) res
~= ";under";
228 if (flags
&Flag
.Overline
) res
~= ";over";
232 import std
.conv
: to
;
233 import std
.ascii
: toLower
;
235 foreach (string s
; __traits(allMembers
, Flag
)) {
237 res
~= "@property bool "~s
[0].toLower
~s
[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return ((flags&Flag."~s
~") != 0); }\n";
238 res
~= "@property void "~s
[0].toLower
~s
[1..$]~" (bool v) pure nothrow @safe @nogc { pragma(inline, true); if (v) flags |= Flag."~s
~"; else flags &= ~Flag."~s
~"; }\n";
242 void resetAttrs () pure nothrow @safe @nogc { pragma(inline
, true); flags
= 0; }
243 bool opEquals() (in auto ref LayFontStyle s
) const pure nothrow @safe @nogc { pragma(inline
, true); return (flags
== s
.flags
&& fontface
== s
.fontface
&& color
== s
.color
&& fontsize
== s
.fontsize
); }
247 // ////////////////////////////////////////////////////////////////////////// //
249 align(1) struct LayLineStyle
{
251 enum Justify
: ubyte {
257 Justify mode
= Justify
.Left
;
258 short lpad
, rpad
, tpad
, bpad
; // paddings; left and right can be negative
259 ubyte paraIndent
; // in spaces
260 string
toString () const {
261 import std
.format
: format
;
263 final switch (mode
) {
264 case Justify
.Left
: res
= "left"; break;
265 case Justify
.Right
: res
= "right"; break;
266 case Justify
.Center
: res
= "center"; break;
267 case Justify
.Justify
: res
= "justify"; break;
269 if (lpad
) res
~= ";lpad:%s".format(lpad
);
270 if (rpad
) res
~= ";rpad:%s".format(rpad
);
271 if (tpad
) res
~= ";tpad:%s".format(tpad
);
272 if (bpad
) res
~= ";bpad:%s".format(bpad
);
276 import std
.conv
: to
;
277 import std
.ascii
: toLower
;
279 foreach (string s
; __traits(allMembers
, Justify
)) {
281 res
~= "@property bool "~s
[0].toLower
~s
[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify."~s
~"); }\n";
282 res
~= "ref LayLineStyle set"~s
~" () pure nothrow @safe @nogc { mode = Justify."~s
~"; return this; }\n";
286 //bool opEquals() (in auto ref LayLineStyle s) const pure nothrow @safe @nogc { pragma(inline, true); return (mode == s.mode && lpad == s.lpad); }
287 @property pure nothrow @safe @nogc {
288 int leftpad () const { pragma(inline
, true); return lpad
; }
289 void leftpad (int v
) { pragma(inline
, true); lpad
= (v
< short.min ?
short.min
: v
> short.max ?
short.max
: cast(short)v
); }
290 int rightpad () const { pragma(inline
, true); return rpad
; }
291 void rightpad (int v
) { pragma(inline
, true); rpad
= (v
< short.min ?
short.min
: v
> short.max ?
short.max
: cast(short)v
); }
292 int toppad () const { pragma(inline
, true); return tpad
; }
293 void toppad (int v
) { pragma(inline
, true); tpad
= (v
< 0 ?
0 : v
> short.max ?
short.max
: cast(short)v
); }
294 int bottompad () const { pragma(inline
, true); return bpad
; }
295 void bottompad (int v
) { pragma(inline
, true); bpad
= (v
< 0 ?
0 : v
> short.max ?
short.max
: cast(short)v
); }
300 // ////////////////////////////////////////////////////////////////////////// //
301 // layouted text word
302 align(1) struct LayWord
{
304 static align(1) struct Props
{
307 CanBreak
= 1<<0, // can i break line at this word?
308 Spaced
= 1<<1, // should this word be whitespaced at the end?
309 Hypen
= 1<<2, // if i'll break at this word, should i add hyphen mark?
310 LineEnd
= 1<<3, // this word ends current line
311 ParaEnd
= 1<<4, // this word ends current paragraph (and, implicitly, line)
312 Object
= 1<<5, // wstart is actually object index in object array
314 // note that if word is softhyphen candidate, i have hyphen mark at [wend]
315 // if props.hyphen is set, wend is including that mark, otherwise it isn't
316 ubyte flags
; // see above
317 @property pure nothrow @safe @nogc:
318 bool canbreak () const { pragma(inline
, true); return ((flags
&Flag
.CanBreak
) != 0); }
319 void canbreak (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.CanBreak
; else flags
&= ~Flag
.CanBreak
; }
320 bool spaced () const { pragma(inline
, true); return ((flags
&Flag
.Spaced
) != 0); }
321 void spaced (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.Spaced
; else flags
&= ~Flag
.Spaced
; }
322 bool hyphen () const { pragma(inline
, true); return ((flags
&Flag
.Hypen
) != 0); }
323 void hyphen (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.Hypen
; else flags
&= ~Flag
.Hypen
; }
324 bool lineend () const { pragma(inline
, true); return ((flags
&Flag
.LineEnd
) != 0); }
325 void lineend (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.LineEnd
; else flags
&= ~Flag
.LineEnd
; }
326 bool paraend () const { pragma(inline
, true); return ((flags
&Flag
.ParaEnd
) != 0); }
327 void paraend (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.ParaEnd
; else flags
&= ~Flag
.ParaEnd
; }
328 bool someend () const { pragma(inline
, true); return ((flags
&(Flag
.ParaEnd|Flag
.LineEnd
)) != 0); }
329 bool object () const { pragma(inline
, true); return ((flags
&Flag
.Object
) != 0); }
330 void object (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.Object
; else flags
&= ~Flag
.Object
; }
332 uint wstart
, wend
; // in LayText text buffer
333 LayFontStyle style
; // font style
334 uint wordNum
; // word number (index in LayText word array)
335 Props propsOrig
; // original properties, used for relayouting
337 Props props
; // effective props after layouting
338 short x
; // horizontal word position in line
339 short h
; // word height (full)
340 short asc
; // ascent (positive)
341 short desc
; // descent (negative)
342 short w
; // word width, without hyphen and spacing
343 short wsp
; // word width with spacing (i.e. with space added at the end)
344 short whyph
; // word width with hyphen (i.e. with hyphen mark added at the end)
345 @property short width () const pure nothrow @safe @nogc { pragma(inline
, true); return (props
.hyphen ? whyph
: w
); }
346 // width with spacing/hyphen
347 @property short fullwidth () const pure nothrow @safe @nogc { pragma(inline
, true); return (props
.hyphen ? whyph
: props
.spaced ? wsp
: w
); }
348 // space width based on original props
349 @property short spacewidth () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(short)(propsOrig
.spaced ? wsp
-w
: 0); }
350 //FIXME: find better place for this! keep that in separate pool, or something, and look there with word index
352 short paraPad
; // to not recalcuate it on each relayouting; set to -1 to recalculate ;-)
353 @property int objectIdx () const pure nothrow @safe @nogc { pragma(inline
, true); return (propsOrig
.object ? wstart
: -1); }
358 // ////////////////////////////////////////////////////////////////////////// //
359 // layouted text line
361 uint wstart
, wend
; // indicies in word array
362 LayLineStyle just
; // line style
363 // calculated properties
364 int x
, y
, w
; // starting x and y positions, width
365 // on finish, layouter will calculate minimal ('cause it is negative) descent
366 int h
, desc
; // height, descent (negative)
367 @property int wordCount () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(int)(wend
-wstart
); }
371 // ////////////////////////////////////////////////////////////////////////// //
372 alias LayText
= LayTextImpl
!char;
375 final class LayTextImpl(TBT
=char) if (is(TBT
== char) ||
is(TBT
== dchar)) {
377 // special control characters
378 enum dchar EndLineCh
= 0x2028; // 0x0085 is treated like whitespace
379 enum dchar EndParaCh
= 0x2029;
382 void ensurePool(ubyte pow2
, bool clear
, T
) (uint want
, ref T
* ptr
, ref uint used
, ref uint alloced
) {
383 if (want
== 0) return;
384 static assert(pow2
< 24, "wtf?!");
385 uint cursz
= used
*cast(uint)T
.sizeof
;
386 if (cursz
>= int.max
/2) throw new Exception("pool overflow");
387 auto lsz
= cast(ulong)want
*T
.sizeof
;
388 if (lsz
>= int.max
/2 || lsz
+cursz
>= int.max
/2) throw new Exception("pool overflow");
389 want
= cast(uint)lsz
;
390 uint cural
= alloced
*cast(uint)T
.sizeof
;
391 if (cursz
+want
> cural
) {
392 import core
.stdc
.stdlib
: realloc
;
394 uint newsz
= ((cursz
+want
)|
((1<<pow2
)-1))+1;
395 if (newsz
>= int.max
/2) throw new Exception("pool overflow");
396 auto np
= cast(T
*)realloc(ptr
, newsz
);
397 if (np
is null) throw new Exception("out of memory for pool");
399 import core
.stdc
.string
: memset
;
400 memset(np
+used
, 0, newsz
-cursz
);
403 alloced
= newsz
/cast(uint)T
.sizeof
;
408 uint charsUsed
, charsAllocated
;
410 static if (is(TBT
== char)) {
411 void putChars (const(dchar)[] str...) {
412 import core
.stdc
.string
: memcpy
;
413 //if (str.length >= int.max/2) throw new Exception("string too long");
415 foreach (dchar ch
; str[]) {
416 //auto len = utf8Encode(buf[], ch);
417 //if (len < 0) { buf.ptr[0] = '?'; len = 1; }
418 if (!Utf8Decoder
.isValidDC(ch
)) ch
= Utf8Decoder
.replacement
;
421 ensurePool
!(16, false)(1, ltext
, charsUsed
, charsAllocated
);
422 ltext
[charsUsed
++] = cast(char)ch
;
426 buf
.ptr
[0] = cast(char)(0xC0|
(ch
>>6));
427 buf
.ptr
[1] = cast(char)(0x80|
(ch
&0x3F));
429 } else if (ch
<= 0xFFFF) {
430 buf
.ptr
[0] = cast(char)(0xE0|
(ch
>>12));
431 buf
.ptr
[1] = cast(char)(0x80|
((ch
>>6)&0x3F));
432 buf
.ptr
[2] = cast(char)(0x80|
(ch
&0x3F));
434 } else if (ch
<= 0x10FFFF) {
435 buf
.ptr
[0] = cast(char)(0xF0|
(ch
>>18));
436 buf
.ptr
[1] = cast(char)(0x80|
((ch
>>12)&0x3F));
437 buf
.ptr
[2] = cast(char)(0x80|
((ch
>>6)&0x3F));
438 buf
.ptr
[3] = cast(char)(0x80|
(ch
&0x3F));
443 ensurePool
!(16, false)(len
, ltext
, charsUsed
, charsAllocated
);
444 memcpy(ltext
+charsUsed
, buf
.ptr
, len
);
451 void putChars (const(dchar)[] str...) {
452 import core
.stdc
.string
: memcpy
;
453 if (str.length
== 0) return;
454 if (str.length
>= int.max
/2/dchar.sizeof
) throw new Exception("string too long");
455 ensurePool
!(16, false)(cast(uint)str.length
, ltext
, charsUsed
, charsAllocated
);
456 memcpy(ltext
+charsUsed
, str.ptr
, cast(uint)str.length
*dchar.sizeof
);
457 charsUsed
+= cast(uint)str.length
;
462 uint wordsUsed
, wordsAllocated
;
464 LayWord
* allocWord(bool clear
=false) () {
465 ensurePool
!(16, true)(1, words
, wordsUsed
, wordsAllocated
);
466 auto res
= words
+wordsUsed
;
468 import core
.stdc
.string
: memset
;
469 memset(res
, 0, (*res
).sizeof
);
471 res
.wordNum
= wordsUsed
++;
472 //res.userTag = wordTag;
477 uint linesUsed
, linesAllocated
;
479 LayLine
* allocLine(bool clear
=false) () {
480 ensurePool
!(16, true)(1, lines
, linesUsed
, linesAllocated
);
482 import core
.stdc
.string
: memset
;
483 auto res
= lines
+(linesUsed
++);
484 memset(res
, 0, (*res
).sizeof
);
487 return lines
+(linesUsed
++);
491 LayLine
* lastLine () { pragma(inline
, true); return (linesUsed
> 0 ? lines
+linesUsed
-1 : null); }
493 bool lastLineHasWords () { pragma(inline
, true); return (linesUsed
> 0 ?
(lines
[linesUsed
-1].wend
> lines
[linesUsed
-1].wstart
) : false); }
495 // should not be called when there are no lines, or no words in last line
496 LayWord
* lastLineLastWord () { pragma(inline
, true); return words
+lastLine
.wend
-1; }
498 static struct StyleStackItem
{
502 StyleStackItem
* styleStack
;
503 uint ststackUsed
, ststackAllocated
;
505 public void pushStyles () {
506 ensurePool
!(4, false)(1, styleStack
, ststackUsed
, ststackAllocated
);
507 auto si
= styleStack
+(ststackUsed
++);
512 public void popStyles () {
513 if (ststackUsed
== 0) throw new Exception("style stack underflow");
514 auto si
= styleStack
+(--ststackUsed
);
520 bool firstParaLine
= true;
521 uint lastWordStart
; // in fulltext
522 uint firstWordNotFlushed
;
524 @property bool hasWordChars () const pure nothrow @safe @nogc { pragma(inline
, true); return (lastWordStart
< charsUsed
); }
527 // current attributes
528 LayLineStyle just
; // for current paragraph
530 // user can change this alot, so don't apply that immediately
531 LayFontStyle newStyle
;
532 LayLineStyle newJust
;
537 bool lastWasSoftHypen
;
538 int maxWidth
; // maximum text width
542 int textHeight
= 0; // total text height
543 int textWidth
= 0; // maximum text width
546 // compare function should return (roughly): key-l
547 alias CmpFn
= int delegate (LayLine
* l
) nothrow @nogc;
549 int findLineBinary (scope CmpFn cmpfn
) {
550 if (linesUsed
== 0) return -1;
551 int bot
= 0, i
= cast(int)linesUsed
-1;
553 int mid
= i
-(i
-bot
)/2;
554 int cmp = cmpfn(lines
+mid
);
555 if (cmp < 0) i
= mid
-1;
556 else if (cmp > 0) bot
= mid
;
559 return (cmpfn(lines
+i
) == 0 ? i
: -1);
562 // find line with this word index
563 int findLineWithWord (uint idx
) {
564 return findLineBinary((LayLine
* l
) {
565 if (idx
< l
.wstart
) return -1;
566 if (idx
>= l
.wend
) return 1;
571 // find line which contains this coordinate
572 int findLineAtY (int y
) {
573 if (linesUsed
== 0) return 0;
575 if (y
>= textHeight
) return cast(int)linesUsed
-1;
576 auto res
= findLineBinary((LayLine
* l
) {
577 if (y
< l
.y
) return -1;
578 if (y
>= l
.y
+l
.h
) return 1;
585 // find word at given coordinate in the given line
586 int findWordAtX (LayLine
* ln
, int x
) {
587 int wcmp (int wnum
) {
588 auto w
= words
+ln
.wstart
+wnum
;
589 if (x
< w
.x
) return -1;
590 return (x
>= (wnum
+1 < ln
.wordCount ? w
[1].x
: w
.x
+w
.w
) ?
1 : 0);
592 if (ln
is null || ln
.wordCount
== 0) return -1;
593 int bot
= 0, i
= ln
.wordCount
-1;
595 int mid
= i
-(i
-bot
)/2;
597 case -1: i
= mid
-1; break;
598 case 1: bot
= mid
; break;
599 default: return ln
.wstart
+mid
;
602 return (wcmp(i
) == 0 ? ln
.wstart
+i
: -1);
605 // find word at the given coordinates
606 int wordAtXY (int x
, int y
) {
607 auto lidx
= findLineAtY(y
);
608 if (lidx
< 0) return -1;
609 auto ln
= lines
+lidx
;
610 if (y
< ln
.y || y
>= ln
.y
+ln
.h || ln
.wordCount
== 0) return -1;
611 return findWordAtX(ln
, x
);
614 LayWord
* wordByIndex (uint idx
) pure nothrow @trusted @nogc { pragma(inline
, true); return (idx
< wordsUsed ? words
+idx
: null); }
616 @property const(TBT
)[] wordText (in ref LayWord w
) const pure nothrow @trusted @nogc { pragma(inline
, true); return (w
.wstart
<= w
.wend ? ltext
[w
.wstart
..w
.wend
] : null); }
618 @property int lineCount () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(int)linesUsed
; }
621 @property auto lineWords (int lidx
) {
622 static struct Range
{
625 int wordsLeft
; // not including current
626 nothrow @trusted @nogc:
628 this(LT
) (LT lay
, int lidx
) {
629 if (lidx
>= 0 && lidx
< lay
.linesUsed
) {
630 auto ln
= lay
.lines
+lidx
;
631 if (ln
.wend
> ln
.wstart
) {
632 w
= lay
.words
+ln
.wstart
;
633 wordsLeft
= ln
.wend
-ln
.wstart
-1;
638 @property bool empty () const pure { pragma(inline
, true); return (w
is null); }
639 @property ref LayWord
front () pure { pragma(inline
, true); assert(w
!is null); return *w
; }
640 void popFront () { if (wordsLeft
) { ++w
; --wordsLeft
; } else w
= null; }
641 Range
save () { Range res
= void; res
.w
= w
; res
.wordsLeft
= wordsLeft
; return res
; }
642 @property int length () const pure { pragma(inline
, true); return (w
!is null ? wordsLeft
+1 : 0); }
643 alias opDollar
= length
;
644 @property LayWord
[] opSlice () { return (w
!is null ? w
[0..wordsLeft
+1] : null); }
645 @property LayWord
[] opSlice (int lo
, int hi
) {
647 if (w
is null || hi
<= lo || lo
> wordsLeft
) return null;
648 if (hi
> wordsLeft
+1) hi
= wordsLeft
+1;
652 return Range(this, lidx
);
655 LayLine
* line (int lidx
) { pragma(inline
, true); return (lidx
>= 0 && lidx
< linesUsed ? lines
+lidx
: null); }
661 this (LayFontStash alaf
, int awidth
) {
662 if (alaf
is null) assert(0, "no layout fonts");
663 if (awidth
< 1) awidth
= 1;
668 ~this () { freeMemory(); }
671 import core
.stdc
.stdlib
: free
;
672 if (lines
!is null) { free(lines
); lines
= null; }
673 if (words
!is null) { free(words
); words
= null; }
674 if (ltext
!is null) { free(ltext
); ltext
= null; }
675 wordsUsed
= wordsAllocated
= linesUsed
= linesAllocated
= charsUsed
= charsAllocated
= 0;
678 @property int width () const pure nothrow @safe @nogc { pragma(inline
, true); return maxWidth
; }
680 // last flushed word index
681 @property uint lastWordIndex () const pure nothrow @safe @nogc { pragma(inline
, true); return (wordsUsed ? wordsUsed
-1 : 0); }
683 // current word index
684 @property uint nextWordIndex () const pure nothrow @safe @nogc { pragma(inline
, true); return wordsUsed
+hasWordChars
; }
686 // use those to change current style. font style will take an effect on next char, line style on next line
687 @property ref LayFontStyle
fontStyle () pure nothrow @safe @nogc { pragma(inline
, true); return newStyle
; }
688 @property ref LayLineStyle
lineStyle () pure nothrow @safe @nogc { pragma(inline
, true); return newJust
; }
690 // return "font id" for the given font face
691 @property int fontFaceId (const(char)[] name
) {
693 int fid
= laf
.fontFaceId(name
);
694 if (fid
>= 0) return fid
;
696 throw new Exception("unknown font face '"~name
.idup
~"'");
699 // return font face for the given "font id"
700 @property string
fontFace (int fid
) { pragma(inline
, true); return (laf
!is null ? laf
.fontFace(fid
) : null); }
703 void endLine () { put(EndLineCh
); }
704 // end current paragraph
705 void endPara () { put(EndParaCh
); }
707 // add "object" into text -- special thing that knows it's dimensions
708 void putObject (LayObject obj
) {
709 import std
.algorithm
: max
, min
;
711 lastWasSoftHypen
= false;
712 if (obj
is null) return;
713 if (objects
.length
>= int.max
/2) throw new Exception("too many objects");
715 // create special word
716 auto w
= allocWord();
717 w
.wstart
= cast(dchar)objects
.length
; // store object index
721 w
.propsOrig
.object
= true;
722 w
.propsOrig
.spaced
= obj
.spaced
;
723 w
.propsOrig
.canbreak
= obj
.canbreak
;
724 w
.props
= w
.propsOrig
;
725 w
.w
= cast(short)min(max(0, obj
.width
), short.max
);
726 w
.whyph
= w
.wsp
= cast(short)min(w
.w
+max(0, obj
.spacewidth
), short.max
);
727 w
.h
= cast(short)min(max(0, obj
.height
), short.max
);
728 w
.asc
= cast(short)min(max(0, obj
.ascent
), short.max
);
729 if (w
.asc
< 0) throw new Exception("object ascent should be positive");
730 w
.desc
= cast(short)min(max(0, obj
.descent
), short.max
);
731 if (w
.desc
> 0) throw new Exception("object descent should be negative");
736 // add text to layouter; it is ok to mix (valid) utf-8 and dchars here
737 void put(T
) (const(T
)[] str...) if (is(T
== char) ||
is(T
== dchar)) {
738 if (str.length
== 0) return;
740 dchar curCh
; // 0: no more chars
743 static if (is(T
== char)) {
745 if (!lastWasUtf
) { lastWasUtf
= true; dec.reset
; }
746 void skipCh () @trusted {
747 while (stpos
< str.length
) {
748 curCh
= dec.decode(cast(ubyte)str.ptr
[stpos
++]);
749 if (curCh
<= dchar.max
) return;
757 void skipCh () @trusted {
758 if (stpos
< str.length
) {
759 curCh
= str.ptr
[stpos
++];
760 if (curCh
> dchar.max
) curCh
= '?';
768 if (!dec.complete
) curCh
= '?'; else skipCh();
774 // process stream dchars
775 if (curCh
== 0) return;
776 if (!hasWordChars
) style
= newStyle
;
777 if (wordsUsed
== 0 || words
[wordsUsed
-1].propsOrig
.someend
) just
= newJust
;
782 if (ch
== EndLineCh || ch
== EndParaCh
) {
783 // ignore leading empty lines
784 if (hasWordChars
) flushWord(); // has some word data, flush it now
785 lastWasSoftHypen
= false; // word flusher is using this flag
786 auto lw
= (wordsUsed ? words
+wordsUsed
-1 : createEmptyWord());
787 // do i need to add empty word for attrs?
788 if (lw
.propsOrig
.someend
) lw
= createEmptyWord();
789 // fix word properties
790 lw
.propsOrig
.canbreak
= true;
791 lw
.propsOrig
.spaced
= false;
792 lw
.propsOrig
.hyphen
= false;
793 lw
.propsOrig
.lineend
= (ch
== EndLineCh
);
794 lw
.propsOrig
.paraend
= (ch
== EndParaCh
);
797 firstParaLine
= (ch
== EndParaCh
);
798 } else if (ch
== 0x00a0) {
799 // non-breaking space
800 lastWasSoftHypen
= false;
801 if (hasWordChars
&& style
!= newStyle
) flushWord();
803 } else if (ch
== 0x0ad) {
805 if (!lastWasSoftHypen
&& hasWordChars
) {
807 lastWasSoftHypen
= true; // word flusher is using this flag
810 lastWasSoftHypen
= true;
811 } else if (ch
<= ' ' ||
isWhite(ch
)) {
814 auto lw
= words
+wordsUsed
-1;
815 lw
.propsOrig
.canbreak
= true;
816 lw
.propsOrig
.spaced
= true;
820 lastWasSoftHypen
= false;
822 lastWasSoftHypen
= false;
823 if (ch
> dchar.max || ch
.isSurrogate || ch
.isPrivateUse || ch
.isNonCharacter || ch
.isMark || ch
.isFormat || ch
.isControl
) ch
= '?';
824 if (hasWordChars
&& style
!= newStyle
) flushWord();
826 if (isDash(ch
) && charsUsed
-lastWordStart
> 1 && !isDash(ltext
[charsUsed
-2])) flushWord();
831 // "finalize" layout: calculate lines, layout words...
832 // call this after you done feeding text to this
835 lastWasSoftHypen
= false;
836 relayout(maxWidth
, true);
839 // relayout everything using the existing words
840 void relayout (int newWidth
, bool forced
) {
841 if (newWidth
< 1) newWidth
= 1;
842 if (!forced
&& newWidth
== maxWidth
) return;
845 if (linesAllocated
> 0) {
846 import core
.stdc
.string
: memset
;
847 memset(lines
, 0, linesAllocated
*lines
[0].sizeof
);
853 firstParaLine
= true;
854 scope(exit
) firstWordNotFlushed
= wu
;
858 auto w
= words
+(lend
++);
859 if (w
.propsOrig
.someend
) break;
861 flushLines(widx
, lend
);
863 firstParaLine
= words
[widx
-1].propsOrig
.paraend
;
869 void save (VFile fl
) {
870 fl
.rawWriteExact("XLL0");
871 fl
.rawWriteExact(ltext
[0..charsUsed
]);
872 fl
.rawWriteExact(words
[0..wordsUsed
]);
877 void dump (VFile fl
) const {
878 fl
.writeln("LINES: ", linesUsed
);
879 foreach (immutable idx
, const ref ln
; lines
[0..linesUsed
]) {
880 fl
.writeln("LINE #", idx
, ": ", ln
.wordCount
, " words; just=", ln
.just
.toString
, "; jlpad=", ln
.just
.leftpad
, "; y=", ln
.y
, "; h=", ln
.h
, "; desc=", ln
.desc
);
881 foreach (immutable widx
, const ref w
; words
[ln
.wstart
..ln
.wend
]) {
882 fl
.writeln(" WORD #", widx
, "(", w
.wordNum
, ")[", w
.wstart
, "..", w
.wend
, "]: ", wordText(w
));
883 fl
.writeln(" wbreak=", w
.props
.canbreak
, "; wspaced=", w
.props
.spaced
, "; whyphen=", w
.props
.hyphen
, "; style=", w
.style
.toString
);
884 fl
.writeln(" x=", w
.x
, "; w=", w
.w
, "; h=", w
.h
, "; asc=", w
.asc
, "; desc=", w
.desc
);
890 static bool isDash (dchar ch
) {
891 pragma(inline
, true);
892 return (ch
== '-' ||
(ch
>= 0x2013 && ch
== 0x2015) || ch
== 0x2212);
895 LayWord
* createEmptyWord () {
896 assert(!hasWordChars
);
897 auto w
= allocWord();
899 w
.props
= w
.propsOrig
;
900 // set word dimensions
901 if (w
.style
.fontface
< 0) throw new Exception("invalid font face in word style");
902 laf
.setFont(w
.style
);
903 w
.w
= w
.wsp
= w
.whyph
= 0;
904 // calculate ascent, descent and height
907 laf
.textMetrics(&a
, &d
, &h
);
908 w
.asc
= cast(short)a
;
909 w
.desc
= cast(short)d
;
920 auto w
= allocWord();
921 w
.wstart
= lastWordStart
;
923 //{ import iv.encoding, std.conv : to; writeln("adding word: [", wordText(*w).to!string.recodeToKOI8, "]"); }
924 w
.propsOrig
.hyphen
= lastWasSoftHypen
;
925 if (lastWasSoftHypen
) {
926 w
.propsOrig
.canbreak
= true;
927 w
.propsOrig
.spaced
= false;
928 --w
.wend
; // remove hyphen mark (for now)
931 w
.props
= w
.propsOrig
;
932 w
.props
.hyphen
= false;
933 // set word dimensions
934 if (w
.style
.fontface
< 0) throw new Exception("invalid font face in word style");
935 laf
.setFont(w
.style
);
936 // i may need spacing later, and anyway most words should be with spacing, so calc it unconditionally
937 if (w
.wend
> w
.wstart
) {
938 auto t
= wordText(*w
);
940 laf
.textWidth2(t
, &ww
, &wsp
, (w
.propsOrig
.hyphen ?
&whyph
: null));
942 w
.wsp
= cast(short)wsp
;
943 if (!w
.propsOrig
.hyphen
) w
.whyph
= w
.w
; else w
.whyph
= cast(short)whyph
;
944 if (isDash(t
[$-1])) { w
.propsOrig
.canbreak
= true; w
.props
.canbreak
= true; }
946 w
.w
= w
.wsp
= w
.whyph
= 0;
948 // calculate ascent, descent and height
951 laf
.textMetrics(&a
, &d
, &h
);
952 w
.asc
= cast(short)a
;
953 w
.desc
= cast(short)d
;
958 lastWordStart
= charsUsed
;
964 void flushLines (uint curw
, uint endw
) {
966 debug(xlay_line_flush
) conwriteln("flushing ", endw
-curw
, " words");
967 uint stline
= linesUsed
; // reformat from this
969 foreach (ref LayWord w
; words
[curw
..endw
]) {
970 if (w
.props
.hyphen
) --w
.wend
; // remove hyphen mark
971 w
.props
= w
.propsOrig
;
972 w
.props
.hyphen
= false;
975 LayWord
* w
= words
+curw
;
976 while (curw
< endw
) {
977 debug(xlay_line_flush
) conwriteln(" ", endw
-curw
, " words left");
979 // add line to work with
981 ln
.wstart
= ln
.wend
= curw
;
983 ln
.w
= w
.just
.leftpad
+w
.just
.rightpad
;
984 // indent first line of paragraph
986 firstParaLine
= false;
987 // left-side or justified lines has paragraph indent
988 if (ln
.just
.paraIndent
> 0 && (w
.just
.left || w
.just
.justify
)) {
989 auto ind
= w
.paraPad
;
991 laf
.setFont(w
.style
);
992 ind
= cast(short)laf
.spacesWidth(ln
.just
.paraIndent
);
996 ln
.just
.leftpad
= ln
.just
.leftpad
+ind
;
1001 //conwriteln("new line; maxWidth=", maxWidth, "; starting line width=", ln.w);
1002 //conwriteln("* maxWidth=", maxWidth, "; ln.w=", ln.w, "; leftpad=", ln.just.leftpad, "; rightpad=", ln.just.rightpad);
1004 debug(xlay_line_flush
) conwritefln
!" (%s:0x%04x) 0x%08x : 0x%08x : 0x%08x : %s"(LayLine
.sizeof
, LayLine
.sizeof
, cast(uint)lines
, cast(uint)ln
, cast(uint)(lines
+linesUsed
-1), cast(int)(ln
-((lines
+linesUsed
-1))));
1005 // add words until i hit breaking point
1006 // if it will end beyond maximum width, and this line
1007 // has some words, flush the line and start new one
1008 uint startIndex
= curw
;
1009 int curwdt
= ln
.w
, lastwsp
= 0;
1010 while (curw
< endw
) {
1011 // add word width with spacing (i will compensate for that after loop)
1012 lastwsp
= (w
.propsOrig
.spaced ? w
.wsp
-w
.w
: 0);
1013 curwdt
+= w
.w
+lastwsp
;
1014 ++curw
; // advance counter here...
1015 if (w
.props
.canbreak
) break; // done with this span
1016 ++w
; // ...and word pointer here (skipping one inc at the end ;-)
1018 debug(xlay_line_flush
) conwriteln(" ", curw
-startIndex
, " words processed");
1019 // can i add the span? if this is first span in line, add it unconditionally
1020 if (ln
.wordCount
== 0 || curwdt
-lastwsp
<= maxWidth
) {
1024 ++w
; // advance to curw
1025 debug(xlay_line_flush
) conwriteln("curwdt=", curwdt
, "; maxWidth=", maxWidth
, "; wc=", ln
.wordCount
, "(", ln
.wend
-ln
.wstart
, ")");
1027 // nope, start new line here
1028 debug(xlay_line_flush
) conwriteln("added line with ", ln
.wordCount
, " words");
1029 // last word in the line should not be spaced
1030 auto ww
= words
+ln
.wend
-1;
1031 // compensate for spacing at last word
1032 ln
.w
-= (ww
.props
.spaced ? ww
.wsp
-ww
.w
: 0);
1033 ww
.props
.spaced
= false;
1034 // and should have hyphen mark if it is necessary
1035 if (ww
.propsOrig
.hyphen
) {
1036 assert(!ww
.props
.hyphen
);
1037 ww
.props
.hyphen
= true;
1039 // fix line width (word layouter will use that)
1040 ln
.w
+= ww
.whyph
-ww
.w
;
1047 debug(xlay_line_flush
) conwriteln("added line with ", ln
.wordCount
, " words; new lines range: [", stline
, "..", linesUsed
, "]");
1048 debug(xlay_line_flush
) conwritefln
!"(%s:0x%04x) 0x%08x : 0x%08x : 0x%08x : %s"(LayLine
.sizeof
, LayLine
.sizeof
, cast(uint)lines
, cast(uint)ln
, cast(uint)(lines
+linesUsed
-1), cast(int)(ln
-((lines
+linesUsed
-1))));
1049 // last line should not be justified
1050 if (ln
.just
.justify
) ln
.just
.setLeft
;
1051 // do real word layouting and fix line metrics
1052 debug(xlay_line_flush
) conwriteln("added ", linesUsed
-stline
, " lines");
1053 foreach (uint lidx
; stline
..linesUsed
) {
1054 debug(xlay_line_flush
) conwriteln(": lidx=", lidx
, "; wc=", lines
[lidx
].wordCount
);
1060 // do word layouting and fix line metrics
1061 void layoutLine (uint lidx
) {
1062 import std
.algorithm
: max
, min
;
1063 assert(lidx
< linesUsed
);
1064 auto ln
= lines
+lidx
;
1065 //conwriteln("maxWidth=", maxWidth, "; ln.w=", ln.w, "; leftpad=", ln.just.leftpad, "; rightpad=", ln.just.rightpad);
1066 debug(xlay_line_layout
) conwriteln("lidx=", lidx
, "; wc=", ln
.wordCount
);
1068 ln
.y
= (lidx ? ln
[-1].y
+ln
[-1].h
: 0);
1069 auto lwords
= lineWords(lidx
);
1070 assert(!lwords
.empty
); // i should have at least one word in each line
1071 // line width is calculated for us by `flushLines()`
1072 // calculate line metrics and number of words with spacing
1073 int lineH
, lineDesc
, wspCount
;
1074 foreach (ref LayWord w
; lwords
.save
) {
1075 lineH
= max(lineH
, w
.h
);
1076 lineDesc
= min(lineDesc
, w
.desc
);
1077 if (w
.props
.spaced
) ++wspCount
;
1079 // vertical padding; clamp it, as i can't have line over line (it will break too many things)
1080 lineH
+= ln
.just
.toppad
+ln
.just
.bottompad
;
1083 if (ln
.w
>= maxWidth
) {
1084 //conwriteln("*** ln.w=", ln.w, "; maxWidth=", maxWidth);
1085 // way too long; (almost) easy deal
1086 // calculate free space to spare in case i'll need to compensate hyphen mark
1087 int x
= ln
.just
.leftpad
, spc
= 0;
1088 foreach (ref LayWord w
; lwords
.save
) {
1091 if (w
.props
.spaced
) spc
+= w
.wsp
-w
.w
;
1093 // if last word ends with hyphen, try to compensate it
1094 if (words
[ln
.wend
-1].props
.hyphen
) {
1095 int needspc
= ln
.w
-maxWidth
;
1096 // no more than 8 pix or 2/3 of free space
1097 if (needspc
<= 8 && needspc
<= spc
/3*2) {
1098 // compensate (i can do fractional math here, but meh...)
1099 while (needspc
> 0) {
1100 // excellence in coding!
1101 foreach_reverse (immutable widx
; ln
.wstart
..ln
.wend
) {
1102 if (words
[widx
].props
.spaced
) {
1104 foreach (immutable c
; widx
+1..ln
.wend
) words
[c
].x
-= 1;
1105 if (--needspc
== 0) break;
1111 } else if (ln
.just
.justify
&& wspCount
> 0) {
1112 // fill the whole line
1113 int spc
= maxWidth
-ln
.w
; // space left to distribute
1114 int xadvsp
= spc
/wspCount
;
1115 int frac
= spc
-xadvsp
*wspCount
;
1116 int x
= ln
.just
.leftpad
;
1117 // no need to save range here, i'll do it in one pass
1118 foreach (ref LayWord w
; lwords
) {
1121 if (w
.props
.spaced
) {
1130 //if (x != maxWidth-ln.just.rightpad) conwriteln("x=", x, "; but it should be ", maxWidth-ln.just.rightpad, "; spcleft=", spc, "; ln.w=", ln.w, "; maxWidth=", maxWidth-ln.w);
1131 //assert(x == maxWidth-ln.just.rightpad);
1134 if (ln
.just
.left || ln
.just
.justify
) x
= ln
.just
.leftpad
;
1135 else if (ln
.just
.right
) x
= maxWidth
-ln
.w
+ln
.just
.leftpad
;
1136 else if (ln
.just
.center
) x
= (maxWidth
-(ln
.w
-ln
.just
.leftpad
-ln
.just
.rightpad
))/2;
1137 else assert(0, "wtf?!");
1138 // no need to save range here, i'll do it in one pass
1139 foreach (ref LayWord w
; lwords
) {
1144 if (ln
.h
< 1) ln
.h
= 1;
1145 textWidth
= max(textWidth
, ln
.w
);
1146 textHeight
= ln
.y
+ln
.h
;
1147 debug(xlay_line_layout
) conwriteln("lidx=", lidx
, "; wc=", ln
.wordCount
);