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/>.
24 import iv
.nanovg
.oui
.blendish
;
31 version(laytest
) import iv
.encoding
;
32 version(aliced
) {} else private alias usize
= size_t
;
35 // ////////////////////////////////////////////////////////////////////////// //
36 // this needs such fonts in stash:
40 // textz -- italic and bold
44 // monoz -- italic and bold
45 final class LayFontStash
{
48 // list of known font faces, should be filled by caller when this object created
50 int[string
] fontfaces
;
54 bool fontWasSet
; // to ensure that first call to `setFont()` will do it's work
55 LayFontStyle lastStyle
;
59 // create new fontstash
60 FONSparams fontParams
;
61 fontParams
.width
= 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
62 fontParams
.height
= 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
63 fontParams
.flags
= FONS_ZERO_TOPLEFT
;
64 fs
= fonsCreateInternal(&fontParams
);
65 if (fs
is null) throw new Exception("error creating font stash");
67 fs
.fonsResetAtlas(1024, 1024);
68 fonsSetSpacing(fs
, 0);
70 fonsSetAlign(fs
, NVGAlign
.Left|NVGAlign
.Baseline
);
73 ~this () { freeFontStash(); }
75 void freeFontStash () {
76 if (killFontStash
&& fs
!is null) {
77 fs
.fonsDeleteInternal();
79 killFontStash
= false;
83 void addFont(T
: const(char)[], TP
: const(char)[]) (T name
, TP path
) {
84 static if (is(T
== typeof(null))) {
85 throw new Exception("invalid font face name");
87 if (name
.length
== 0) throw new Exception("invalid font face name");
88 if (name
in fontfaces
) throw new Exception("duplicate font '"~name
.idup
~"'");
89 int fid
= fs
.fonsAddFont(name
, path
);
90 if (fid
< 0) throw new Exception("font '"~name
~"' is not found at '"~path
.idup
~"'");
91 static if (is(T
== string
)) fontfaces
[name
] = fid
; else fontfaces
[name
.idup
] = fid
;
95 @property int fontFace (const(char)[] name
) {
96 if (auto fid
= name
in fontfaces
) return *fid
;
100 void setFont() (in auto ref LayFontStyle style
) {
101 int fsz
= style
.fontsize
;
102 if (fsz
< 1) fsz
= 1;
103 if (!fontWasSet || fsz
!= lastStyle
.fontsize || style
.fontface
!= lastStyle
.fontface
) {
104 if (style
.fontface
!= lastStyle
.fontface
) fonsSetFont(fs
, style
.fontface
);
105 if (fsz
!= lastStyle
.fontsize
) fonsSetSize(fs
, fsz
);
107 lastStyle
.fontsize
= fsz
;
111 int textWidth(T
) (const(T
)[] str) if (is(T
== char) ||
is(T
== dchar)) {
112 import std
.algorithm
: max
;
113 import core
.stdc
.math
: lrintf
;
115 float adv
= fs
.fonsTextBounds(0, 0, str, b
[]);
117 return lrintf(max(adv
, w
));
120 int spacesWidth (int count
) {
121 import core
.stdc
.math
: lrintf
;
122 if (count
< 1) return 0;
123 auto it
= FonsTextBoundsIterator(fs
, 0, 0);
125 return lrintf(it
.advance
*count
);
128 void textWidth2(T
) (const(T
)[] str, int* w
=null, int* wsp
=null, int* whyph
=null) if (is(T
== char) ||
is(T
== dchar)) {
129 import core
.stdc
.math
: lrintf
;
130 import std
.algorithm
: max
;
131 if (w
is null && wsp
is null && whyph
is null) return;
133 auto it
= FonsTextBoundsIterator(fs
, 0, 0);
136 it
.getHBounds(minx
, maxx
);
137 *w
= lrintf(max(it
.advance
, maxx
-minx
));
139 if (wsp
!is null && whyph
is null) {
141 it
.getHBounds(minx
, maxx
);
142 *wsp
= lrintf(max(it
.advance
, maxx
-minx
));
143 } else if (wsp
is null && whyph
!is null) {
144 it
.put(cast(dchar)45);
145 it
.getHBounds(minx
, maxx
);
146 *whyph
= lrintf(max(it
.advance
, maxx
-minx
));
147 } else if (wsp
!is null && whyph
!is null) {
150 it
.getHBounds(minx
, maxx
);
151 *wsp
= lrintf(max(it
.advance
, maxx
-minx
));
152 sit
.put(cast(dchar)45);
153 sit
.getHBounds(minx
, maxx
);
154 *whyph
= lrintf(max(sit
.advance
, maxx
-minx
));
159 import core
.stdc
.math
: lrintf
;
160 // use line bounds for height
161 float y0
= void, y1
= void;
162 fs
.fonsLineBounds(0, &y0
, &y1
);
163 return lrintf(y1
-y0
);
166 void textMetrics (int* asc
, int* desc
, int* lineh
) {
167 import core
.stdc
.math
: lrintf
;
168 float a
= void, d
= void, h
= void;
169 fs
.fonsVertMetrics(&a
, &d
, &h
);
170 if (asc
!is null) *asc
= lrintf(a
);
171 if (desc
!is null) *desc
= lrintf(d
);
172 if (lineh
!is null) *lineh
= lrintf(h
);
177 // ////////////////////////////////////////////////////////////////////////// //
178 // generic text style
179 align(1) struct LayFontStyle
{
188 ubyte flags
; // see above
189 int fontface
= -1; // i can't use strings here, as this struct inside LayWord will not be GC-scanned
191 uint color
= 0xff000000; // AABBGGRR; AA usually ignored by renderer, but i'll keep it anyway
192 string
toString () const {
193 import std
.format
: format
;
194 string res
= "font:%s;size:%s;color:0x%08X".format(fontface
, fontsize
, color
);
195 if (flags
&Flag
.Italic
) res
~= ";italic";
196 if (flags
&Flag
.Bold
) res
~= ";bold";
197 if (flags
&Flag
.Strike
) res
~= ";strike";
198 if (flags
&Flag
.Underline
) res
~= ";under";
199 if (flags
&Flag
.Overline
) res
~= ";over";
203 import std
.conv
: to
;
204 import std
.ascii
: toLower
;
206 foreach (string s
; __traits(allMembers
, Flag
)) {
208 res
~= "@property bool "~s
[0].toLower
~s
[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return ((flags&Flag."~s
~") != 0); }\n";
209 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";
213 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
); }
217 // ////////////////////////////////////////////////////////////////////////// //
219 align(1) struct LayLineStyle
{
221 enum Justify
: ubyte {
227 Justify mode
= Justify
.Left
;
228 short lpad
, rpad
, tpad
, bpad
;
229 ubyte paraIndent
; // in spaces
230 string
toString () const {
231 import std
.format
: format
;
233 final switch (mode
) {
234 case Justify
.Left
: res
= "left"; break;
235 case Justify
.Right
: res
= "right"; break;
236 case Justify
.Center
: res
= "center"; break;
237 case Justify
.Justify
: res
= "justify"; break;
239 if (lpad
) res
~= ";lpad:%s".format(lpad
);
240 if (rpad
) res
~= ";rpad:%s".format(rpad
);
241 if (tpad
) res
~= ";tpad:%s".format(tpad
);
242 if (bpad
) res
~= ";bpad:%s".format(bpad
);
246 import std
.conv
: to
;
247 import std
.ascii
: toLower
;
249 foreach (string s
; __traits(allMembers
, Justify
)) {
251 res
~= "@property bool "~s
[0].toLower
~s
[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify."~s
~"); }\n";
252 res
~= "ref LayLineStyle set"~s
~" () pure nothrow @safe @nogc { mode = Justify."~s
~"; return this; }\n";
256 bool opEquals() (in auto ref LayLineStyle s
) const pure nothrow @safe @nogc { pragma(inline
, true); return (mode
== s
.mode
&& lpad
== s
.lpad
); }
257 @property pure nothrow @safe @nogc {
258 int leftpad () const { pragma(inline
, true); return lpad
; }
259 void leftpad (int v
) { pragma(inline
, true); lpad
= (v
< short.min ?
short.min
: v
> short.max ?
short.max
: cast(short)v
); }
260 int rightpad () const { pragma(inline
, true); return rpad
; }
261 void rightpad (int v
) { pragma(inline
, true); rpad
= (v
< short.min ?
short.min
: v
> short.max ?
short.max
: cast(short)v
); }
262 int toppad () const { pragma(inline
, true); return tpad
; }
263 void toppad (int v
) { pragma(inline
, true); tpad
= (v
< 0 ?
0 : v
> short.max ?
short.max
: cast(short)v
); }
264 int bottompad () const { pragma(inline
, true); return bpad
; }
265 void bottompad (int v
) { pragma(inline
, true); bpad
= (v
< 0 ?
0 : v
> short.max ?
short.max
: cast(short)v
); }
270 // ////////////////////////////////////////////////////////////////////////// //
271 // layouted text word
272 align(1) struct LayWord
{
274 static align(1) struct Props
{
277 CanBreak
= 1<<0, // can i break line at this word?
278 Spaced
= 1<<1, // should this word be whitespaced at the end?
279 Hypen
= 1<<2, // if i'll break at this word, should i add hyphen mark?
280 LineEnd
= 1<<3, // this word ends current line
281 ParaEnd
= 1<<4, // this word ends current paragraph (and, implicitly, line)
283 ubyte flags
; // see above
284 @property pure nothrow @safe @nogc:
285 bool canbreak () const { pragma(inline
, true); return ((flags
&Flag
.CanBreak
) != 0); }
286 void canbreak (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.CanBreak
; else flags
&= ~Flag
.CanBreak
; }
287 bool spaced () const { pragma(inline
, true); return ((flags
&Flag
.Spaced
) != 0); }
288 void spaced (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.Spaced
; else flags
&= ~Flag
.Spaced
; }
289 bool hyphen () const { pragma(inline
, true); return ((flags
&Flag
.Hypen
) != 0); }
290 void hyphen (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.Hypen
; else flags
&= ~Flag
.Hypen
; }
291 bool lineend () const { pragma(inline
, true); return ((flags
&Flag
.LineEnd
) != 0); }
292 void lineend (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.LineEnd
; else flags
&= ~Flag
.LineEnd
; }
293 bool paraend () const { pragma(inline
, true); return ((flags
&Flag
.ParaEnd
) != 0); }
294 void paraend (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.ParaEnd
; else flags
&= ~Flag
.ParaEnd
; }
295 // note that if word is softhyphen candidate, i have hyphen mark at [wend]
296 // if props.hyphen is set, wend is including that mark, otherwise it isn't
298 uint wstart
, wend
; // in LayText text buffer
299 LayFontStyle style
; // font style
300 uint wordNum
; // word number (index in LayText word array)
301 Props propsOrig
; // original properties, used for relayouting
303 Props props
; // effective props after layouting
304 int x
; // horizontal word position in line
305 int h
; // word height (full)
306 int asc
; // ascent (positive)
307 int desc
; // descent (negative)
308 int w
; // word width, without hyphen and spacing
309 int wsp
; // word width with spacing (i.e. with space added at the end)
310 int whyph
; // word width with hyphen (i.e. with hyphen mark added at the end)
311 @property int width () const pure nothrow @safe @nogc { pragma(inline
, true); return (props
.hyphen ? whyph
: w
); }
312 // width with spacing/hyphen
313 @property int fullwidth () const pure nothrow @safe @nogc { pragma(inline
, true); return (props
.hyphen ? whyph
: props
.spaced ? wsp
: w
); }
314 // space width based on original props
315 @property int spacewidth () const pure nothrow @safe @nogc { pragma(inline
, true); return (propsOrig
.spaced ? wsp
-w
: 0); }
316 //FIXME: find better place for this! keep that in separate pool, or something, and look there with word index
321 // ////////////////////////////////////////////////////////////////////////// //
322 // layouted text line
324 uint wstart
, wend
; // indicies in word array
325 LayLineStyle just
; // line style
326 // calculated properties
327 int x
, y
, w
; // starting x and y positions, width
328 // on finish, layouter will calculate minimal ('cause it is negative) descent
329 int h
, desc
; // height, descent (negative)
330 @property int wordCount () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(int)(wend
-wstart
); }
334 // ////////////////////////////////////////////////////////////////////////// //
336 final class LayText
{
338 // special control characters
339 enum dchar EndLineCh
= 0x2028; // 0x0085 is treated like whitespace
340 enum dchar EndParaCh
= 0x2029;
343 void ensurePool(ubyte pow2
, bool clear
, T
) (uint want
, ref T
* ptr
, ref uint used
, ref uint alloced
) {
344 if (want
== 0) return;
345 static assert(pow2
< 24, "wtf?!");
346 uint cursz
= used
*cast(uint)T
.sizeof
;
347 if (cursz
>= int.max
/2) throw new Exception("pool overflow");
348 auto lsz
= cast(ulong)want
*T
.sizeof
;
349 if (lsz
>= int.max
/2 || lsz
+cursz
>= int.max
/2) throw new Exception("pool overflow");
350 want
= cast(uint)lsz
;
351 uint cural
= alloced
*cast(uint)T
.sizeof
;
352 if (cursz
+want
> cural
) {
353 import core
.stdc
.stdlib
: realloc
;
355 uint newsz
= ((cursz
+want
)|
((1<<pow2
)-1))+1;
356 if (newsz
>= int.max
/2) throw new Exception("pool overflow");
357 auto np
= cast(T
*)realloc(ptr
, newsz
);
358 if (np
is null) throw new Exception("out of memory for pool");
360 import core
.stdc
.string
: memset
;
361 memset(np
+used
, 0, newsz
-cursz
);
364 alloced
= newsz
/cast(uint)T
.sizeof
;
369 uint charsUsed
, charsAllocated
;
371 void putChars (const(dchar)[] str...) {
372 import core
.stdc
.string
: memcpy
;
373 if (str.length
== 0) return;
374 if (str.length
> int.max
/2) throw new Exception("text too big");
375 ensurePool
!(16, false)(str.length
, ltext
, charsUsed
, charsAllocated
);
376 memcpy(ltext
+charsUsed
, str.ptr
, cast(uint)str.length
*cast(uint)ltext
[0].sizeof
);
377 charsUsed
+= cast(uint)str.length
;
381 uint wordsUsed
, wordsAllocated
;
383 LayWord
* allocWord () {
384 ensurePool
!(16, true)(1, words
, wordsUsed
, wordsAllocated
);
385 auto res
= words
+wordsUsed
;
386 res
.wordNum
= wordsUsed
++;
391 uint linesUsed
, linesAllocated
;
393 LayLine
* allocLine () {
394 ensurePool
!(16, true)(1, lines
, linesUsed
, linesAllocated
);
395 return lines
+(linesUsed
++);
398 LayLine
* lastLine () { pragma(inline
, true); return (linesUsed
> 0 ? lines
+linesUsed
-1 : null); }
400 bool lastLineHasWords () { pragma(inline
, true); return (linesUsed
> 0 ?
(lines
[linesUsed
-1].wend
> lines
[linesUsed
-1].wstart
) : false); }
402 // should not be called when there are no lines, or no words in last line
403 LayWord
* lastLineLastWord () { pragma(inline
, true); return words
+lastLine
.wend
-1; }
405 static struct StyleStackItem
{
409 StyleStackItem
* styleStack
;
410 uint ststackUsed
, ststackAllocated
;
412 public void pushStyles () {
413 ensurePool
!(4, false)(1, styleStack
, ststackUsed
, ststackAllocated
);
414 auto si
= styleStack
+(ststackUsed
++);
419 public void popStyles () {
420 if (ststackUsed
== 0) throw new Exception("style stack underflow");
421 auto si
= styleStack
+(--ststackUsed
);
427 bool firstParaLine
= true;
428 uint lastWordStart
; // in fulltext
429 uint firstWordNotFlushed
;
431 @property bool hasWordChars () const pure nothrow @safe @nogc { pragma(inline
, true); return (lastWordStart
< charsUsed
); }
434 // current attributes
435 LayLineStyle just
; // for current paragraph
437 // user can change this alot, so don't apply that immediately
438 LayFontStyle newStyle
;
439 LayLineStyle newJust
;
444 bool lastWasSoftHypen
;
445 int maxWidth
; // maximum text width
449 int textHeight
= 0; // total text height
450 int textWidth
= 0; // maximum text width
453 // compare function should return (roughly): key-l
454 alias CmpFn
= int delegate (LayLine
* l
) nothrow @nogc;
456 int findLineBinary (scope CmpFn cmpfn
) {
457 if (linesUsed
== 0) return -1;
458 int bot
= 0, i
= cast(int)linesUsed
-1;
460 int mid
= i
-(i
-bot
)/2;
461 int cmp = cmpfn(lines
+mid
);
462 if (cmp < 0) i
= mid
-1;
463 else if (cmp > 0) bot
= mid
;
466 return (cmpfn(lines
+i
) == 0 ? i
: -1);
469 // find line with this word index
470 int findLineWithWord (uint idx
) {
471 return findLineBinary((LayLine
* l
) {
472 if (idx
< l
.wstart
) return -1;
473 if (idx
>= l
.wend
) return 1;
478 // find line which contains this coordinate
479 int findLineWithY (int y
) {
480 if (linesUsed
== 0) return 0;
482 if (y
>= textHeight
) return cast(int)linesUsed
-1;
483 auto res
= findLineBinary((LayLine
* l
) {
484 if (y
< l
.y
) return -1;
485 if (y
>= l
.y
+l
.h
) return 1;
488 //if (res == -1) { import std.stdio; writeln("*** y=", y, "; th=", textHeight); }
493 @property const(dchar)[] wordText (in ref LayWord w
) const pure nothrow @trusted @nogc { pragma(inline
, true); return ltext
[w
.wstart
..w
.wend
]; }
495 @property int lineCount () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(int)linesUsed
; }
498 @property auto lineWords (int lidx
) {
499 static struct Range
{
502 int wordsLeft
; // not including current
503 nothrow @trusted @nogc:
505 this (LayText lay
, int lidx
) {
506 if (lidx
>= 0 && lidx
< lay
.linesUsed
) {
507 auto ln
= lay
.lines
+lidx
;
508 if (ln
.wend
> ln
.wstart
) {
509 w
= lay
.words
+ln
.wstart
;
510 wordsLeft
= ln
.wend
-ln
.wstart
-1;
515 @property bool empty () const pure { pragma(inline
, true); return (w
is null); }
516 //@property ref LayWord front () pure { pragma(inline, true); assert(w !is null); return *w; }
517 @property ref LayWord
front () pure { pragma(inline
, true); assert(w
!is null); return *w
; }
518 void popFront () { if (wordsLeft
) { ++w
; --wordsLeft
; } else w
= null; }
519 Range
save () { Range res
= void; res
.w
= w
; res
.wordsLeft
= wordsLeft
; return res
; }
520 @property int length () const pure { pragma(inline
, true); return (w
!is null ? wordsLeft
+1 : 0); }
521 alias opDollar
= length
;
522 @property LayWord
[] opSlice () { return (w
!is null ? w
[0..wordsLeft
+1] : null); }
523 @property LayWord
[] opSlice (int lo
, int hi
) {
525 if (w
is null || hi
<= lo || lo
> wordsLeft
) return null;
526 if (hi
> wordsLeft
+1) hi
= wordsLeft
+1;
530 return Range(this, lidx
);
533 LayLine
* line (int lidx
) { pragma(inline
, true); return (lidx
>= 0 && lidx
< linesUsed ? lines
+lidx
: null); }
536 this (LayFontStash alaf
, int awidth
) {
537 if (alaf
is null) assert(0, "no layout fonts");
538 if (awidth
< 1) awidth
= 1;
543 ~this () { freeMemory(); }
546 import core
.stdc
.stdlib
: free
;
547 if (lines
!is null) { free(lines
); lines
= null; }
548 if (words
!is null) { free(words
); words
= null; }
549 if (ltext
!is null) { free(ltext
); ltext
= null; }
550 wordsUsed
= wordsAllocated
= linesUsed
= linesAllocated
= charsUsed
= charsAllocated
= 0;
553 @property int width () const pure nothrow @safe @nogc { pragma(inline
, true); return maxWidth
; }
555 @property uint curWordIndex () const pure nothrow @safe @nogc { pragma(inline
, true); return (wordsUsed ? wordsUsed
-1 : 0); }
557 @property ref LayFontStyle
fontStyle () pure nothrow @safe @nogc { pragma(inline
, true); return newStyle
; }
558 @property ref LayLineStyle
lineStyle () pure nothrow @safe @nogc { pragma(inline
, true); return newJust
; }
560 @property int fontFace (const(char)[] name
) {
562 int fid
= laf
.fontFace(name
);
563 if (fid
>= 0) return fid
;
565 throw new Exception("unknown font face '"~name
.idup
~"'");
568 void endLine () { put(EndLineCh
); }
569 void endPara () { put(EndParaCh
); }
571 // add text to layouter
572 void put(T
) (const(T
)[] str...) if (is(T
== char) ||
is(T
== dchar)) {
573 if (str.length
== 0) return;
575 dchar curCh
; // 0: no more chars
578 static if (is(T
== char)) {
580 if (!lastWasUtf
) { lastWasUtf
= true; dec.reset
; }
581 void skipCh () @trusted {
582 while (stpos
< str.length
) {
583 curCh
= dec.decode(cast(ubyte)str.ptr
[stpos
++]);
584 if (curCh
<= dchar.max
) return;
592 void skipCh () @trusted {
593 if (stpos
< str.length
) {
594 curCh
= str.ptr
[stpos
++];
595 if (curCh
> dchar.max
) curCh
= '?';
603 if (!dec.complete
) curCh
= '?'; else skipCh();
609 // process stream dchars
610 if (curCh
== 0) return;
611 if (!hasWordChars
) style
= newStyle
;
612 if (firstWordNotFlushed
>= wordsUsed
) just
= newJust
;
617 if (ch
== EndLineCh || ch
== EndParaCh
) {
618 lastWasSoftHypen
= false;
619 // ignore leading empty lines
620 if (hasWordChars || linesUsed
) {
623 // has some word data, flush it now
625 lw
= words
+wordsUsed
-1;
626 } else if (wordsUsed
== 0) {
627 // create empty word to set attributes on it
630 lw
= words
+wordsUsed
-1;
631 // do i need to add empty word for attrs?
632 if (lw
.propsOrig
.lineend || lw
.propsOrig
.paraend
) lw
= allocWord();
634 // fix word properties
635 lw
.propsOrig
.canbreak
= true;
636 lw
.propsOrig
.spaced
= false;
637 lw
.propsOrig
.hyphen
= false;
638 lw
.propsOrig
.lineend
= (ch
== EndLineCh
);
639 lw
.propsOrig
.paraend
= (ch
== EndParaCh
);
643 if (ch
== EndParaCh
) just
= newJust
;
644 firstParaLine
= (ch
== EndParaCh
);
645 } else if (ch
== 0x00a0) {
646 // non-breaking space
647 lastWasSoftHypen
= false;
648 if (hasWordChars
&& style
!= newStyle
) flushWord();
650 } else if (ch
== 0x0ad) {
652 if (!lastWasSoftHypen
&& hasWordChars
) {
654 lastWasSoftHypen
= true; // word flusher is using this flag
657 lastWasSoftHypen
= true;
658 } else if (ch
<= ' ' ||
isWhite(ch
)) {
659 lastWasSoftHypen
= false;
662 auto lw
= words
+wordsUsed
-1;
663 lw
.propsOrig
.canbreak
= true;
664 lw
.propsOrig
.spaced
= true;
669 lastWasSoftHypen
= false;
670 if (ch
> dchar.max || ch
.isSurrogate || ch
.isPrivateUse || ch
.isNonCharacter || ch
.isMark || ch
.isFormat || ch
.isControl
) ch
= '?';
671 if (hasWordChars
&& style
!= newStyle
) flushWord();
673 if (isDash(ch
) && charsUsed
-lastWordStart
> 1 && !isDash(ltext
[charsUsed
-2])) flushWord();
684 void save (VFile fl
) {
685 fl
.rawWriteExact("");
689 void dump (VFile fl
) const {
690 fl
.writeln("LINES: ", linesUsed
);
691 foreach (immutable idx
, const ref ln
; lines
[0..linesUsed
]) {
692 fl
.writeln("LINE #", idx
, ": ", ln
.wordCount
, " words; just=", ln
.just
.toString
, "; jlpad=", ln
.just
.lpad
, "; y=", ln
.y
, "; h=", ln
.h
, "; desc=", ln
.desc
);
693 foreach (immutable widx
, const ref w
; words
[ln
.wstart
..ln
.wend
]) {
694 fl
.writeln(" WORD #", widx
, "(", w
.wordNum
, ")[", w
.wstart
, "..", w
.wend
, "]: ", wordText(w
));
695 fl
.writeln(" wbreak=", w
.props
.canbreak
, "; wspaced=", w
.props
.spaced
, "; whyphen=", w
.props
.hyphen
, "; style=", w
.style
.toString
);
696 fl
.writeln(" x=", w
.x
, "; w=", w
.w
, "; h=", w
.h
, "; asc=", w
.asc
, "; desc=", w
.desc
);
702 static bool isDash (dchar ch
) {
703 pragma(inline
, true);
704 return (ch
== '-' ||
(ch
>= 0x2013 && ch
== 0x2015) || ch
== 0x2212);
709 auto w
= allocWord();
710 w
.wstart
= lastWordStart
;
712 //{ import iv.encoding, std.conv : to; writeln("adding word: [", wordText(*w).to!string.recodeToKOI8, "]"); }
713 w
.propsOrig
.hyphen
= lastWasSoftHypen
;
714 if (lastWasSoftHypen
) {
715 w
.propsOrig
.canbreak
= true;
716 w
.propsOrig
.spaced
= false;
717 --w
.wend
; // remove hyphen mark (for now)
720 w
.props
= w
.propsOrig
;
721 w
.props
.hyphen
= false;
722 // set word dimensions
723 if (w
.style
.fontface
< 0) throw new Exception("invalid font face in word style");
724 laf
.setFont(w
.style
);
725 // i may need spacing later, and anyway most words should be with spacing, so calc it unconditionally
726 if (w
.wend
> w
.wstart
) {
727 auto t
= wordText(*w
);
728 laf
.textWidth2(t
, &w
.w
, &w
.wsp
, (w
.propsOrig
.hyphen ?
&w
.whyph
: null));
729 if (!w
.propsOrig
.hyphen
) w
.whyph
= w
.w
;
730 if (isDash(t
[$-1])) { w
.propsOrig
.canbreak
= true; w
.props
.canbreak
= true; }
732 w
.w
= w
.wsp
= w
.whyph
= 0;
734 // calculate ascent, descent and height
735 laf
.textMetrics(&w
.asc
, &w
.desc
, &w
.h
);
737 lastWordStart
= charsUsed
;
743 uint curw
= firstWordNotFlushed
;
744 uint endw
= wordsUsed
;
746 debug(xlay_line_flush
) writeln("flushing ", endw
-curw
, " words");
747 uint stline
= linesUsed
; // reformat from this
749 foreach (ref LayWord w
; words
[curw
..endw
]) {
750 if (w
.props
.hyphen
) --w
.wend
; // remove hyphen mark
751 w
.props
= w
.propsOrig
;
752 w
.props
.hyphen
= false;
755 LayWord
* w
= words
+curw
;
756 while (curw
< endw
) {
757 debug(xlay_line_flush
) writeln(" ", endw
-curw
, " words left");
759 // add line to work with
761 ln
.wstart
= ln
.wend
= curw
;
763 ln
.w
= just
.lpad
+just
.rpad
;
764 // indent first line of paragraph
766 firstParaLine
= false;
767 // left-side or justified lines has paragraph indent
768 if (ln
.just
.paraIndent
> 0 && (just
.left || just
.justify
)) {
769 laf
.setFont(w
.style
);
770 int ind
= laf
.spacesWidth(ln
.just
.paraIndent
);
775 //writeln("new line; maxWidth=", maxWidth, "; starting line width=", ln.w);
777 debug(xlay_line_flush
) writefln(" (%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))));
778 // add words until i hit breaking point
779 // if it will end beyond maximum width, and this line
780 // has some words, flush the line and start new one
781 uint startIndex
= curw
;
782 int curwdt
= ln
.w
, lastwsp
= 0;
783 while (curw
< endw
) {
784 // add word width with spacing (i will compensate for that after loop)
785 lastwsp
= (w
.propsOrig
.spaced ? w
.wsp
-w
.w
: 0);
786 curwdt
+= w
.w
+lastwsp
;
787 ++curw
; // advance counter here...
788 if (w
.props
.canbreak
) break; // done with this span
789 ++w
; // ...and word pointer here (skipping one inc at the end ;-)
791 debug(xlay_line_flush
) writeln(" ", curw
-startIndex
, " words processed");
792 // can i add the span? if this is first span in line, add it unconditionally
793 if (ln
.wordCount
== 0 || curwdt
-lastwsp
<= maxWidth
) {
797 ++w
; // advance to curw
798 debug(xlay_line_flush
) writeln("curwdt=", curwdt
, "; maxWidth=", maxWidth
, "; wc=", ln
.wordCount
, "(", ln
.wend
-ln
.wstart
, ")");
800 // nope, start new line here
801 debug(xlay_line_flush
) writeln("added line with ", ln
.wordCount
, " words");
802 // last word in the line should not be spaced
803 auto ww
= words
+ln
.wend
-1;
804 // compensate for spacing at last word
806 ww
.props
.spaced
= false;
807 // and should have hyphen mark if it is necessary
808 if (ww
.propsOrig
.hyphen
) {
809 assert(!ww
.props
.hyphen
);
810 ww
.props
.hyphen
= true;
812 // fix line width (word layouter will use that)
813 ln
.w
+= ww
.whyph
-ww
.w
;
820 debug(xlay_line_flush
) writeln("added line with ", ln
.wordCount
, " words; new lines range: [", stline
, "..", linesUsed
, "]");
821 debug(xlay_line_flush
) writefln("(%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))));
822 // last line should not be justified
823 if (just
.justify
) ln
.just
.setLeft
;
824 // do real word layouting and fix line metrics
825 debug(xlay_line_flush
) writeln("added ", linesUsed
-stline
, " lines");
826 foreach (uint lidx
; stline
..linesUsed
) {
827 debug(xlay_line_flush
) writeln(": lidx=", lidx
, "; wc=", lines
[lidx
].wordCount
);
830 firstWordNotFlushed
= endw
;
834 // do word layouting and fix line metrics
835 void layoutLine (uint lidx
) {
836 import std
.algorithm
: max
, min
;
837 assert(lidx
< linesUsed
);
838 auto ln
= lines
+lidx
;
839 debug(xlay_line_layout
) writeln("lidx=", lidx
, "; wc=", ln
.wordCount
);
841 ln
.y
= (lidx ? ln
[-1].y
+ln
[-1].h
: 0);
842 auto lwords
= lineWords(lidx
);
843 assert(!lwords
.empty
); // i should have at least one word in each line
844 // line width is calculated for us by `flushLines()`
845 // calculate line metrics and number of words with spacing
846 int lineH
, lineDesc
, wspCount
;
847 foreach (ref LayWord w
; lwords
.save
) {
848 lineH
= max(lineH
, w
.h
);
849 lineDesc
= min(lineDesc
, w
.desc
);
850 if (w
.props
.spaced
) ++wspCount
;
852 // vertical padding; clamp it, as i can't have line over line (it will break too many things)
853 lineH
+= max(0, ln
.just
.tpad
)+max(0, ln
.just
.bpad
);
856 if (ln
.w
>= maxWidth
) {
857 // way too long; (almost) easy deal
858 // calculate free space to spare in case i'll need to compensate hyphen mark
859 int x
= ln
.just
.lpad
, spc
= 0;
860 foreach (ref LayWord w
; lwords
.save
) {
863 if (w
.props
.spaced
) spc
+= w
.wsp
-w
.w
;
865 // if last word ends with hyphen, try to compensate it
866 if (words
[ln
.wend
-1].props
.hyphen
) {
867 int needspc
= ln
.w
-maxWidth
;
868 // no more than 8 pix or 2/3 of free space
869 if (needspc
<= 8 && needspc
<= spc
/3*2) {
870 // compensate (i can do fractional math here, but meh...)
871 while (needspc
> 0) {
872 // excellence in coding!
873 foreach_reverse (immutable widx
; ln
.wstart
..ln
.wend
) {
874 if (words
[widx
].props
.spaced
) {
876 foreach (immutable c
; widx
+1..ln
.wend
) words
[c
].x
-= 1;
877 if (--needspc
== 0) break;
883 } else if (ln
.just
.justify
&& wspCount
> 0) {
884 // fill the whole line
885 int spc
= maxWidth
-ln
.w
; // space left to distribute
886 int xadvsp
= spc
/wspCount
;
887 int frac
= spc
-xadvsp
*wspCount
;
888 int x
= ln
.just
.lpad
;
889 // no need to save range here, i'll do it in one pass
890 foreach (ref LayWord w
; lwords
) {
893 if (w
.props
.spaced
) {
900 if (ln
.just
.left || ln
.just
.justify
) x
= ln
.just
.lpad
;
901 else if (ln
.just
.right
) x
= maxWidth
-ln
.w
+ln
.just
.lpad
;
902 else if (ln
.just
.center
) x
= (maxWidth
-(ln
.w
-ln
.just
.lpad
-ln
.just
.rpad
))/2;
903 else assert(0, "wtf?!");
904 // no need to save range here, i'll do it in one pass
905 foreach (ref LayWord w
; lwords
) {
910 if (ln
.h
< 1) ln
.h
= 1;
911 textWidth
= max(textWidth
, ln
.w
);
912 textHeight
= ln
.y
+ln
.h
;
913 debug(xlay_line_layout
) writeln("lidx=", lidx
, "; wc=", ln
.wordCount
);