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/>.
27 import iv
.nanovg
.oui
.blendish
;
32 version(laytest
) import iv
.encoding
;
33 version(aliced
) {} else private alias usize
= size_t
;
36 // ////////////////////////////////////////////////////////////////////////// //
48 import std
.ascii
: toLower
;
50 foreach (string s
; __traits(allMembers
, Flag
)) {
52 res
~= "@property bool "~s
[0].toLower
~s
[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return ((flags&Flag."~s
~") != 0); }\n";
53 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";
57 bool opEquals() (in auto ref LayStyle s
) const pure nothrow @safe @nogc { pragma(inline
, true); return (flags
== s
.flags
); }
61 // ////////////////////////////////////////////////////////////////////////// //
63 enum Justify
: ubyte {
69 Justify mode
= Justify
.Left
;
73 import std
.ascii
: toLower
;
75 foreach (string s
; __traits(allMembers
, Justify
)) {
77 res
~= "@property bool "~s
[0].toLower
~s
[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify."~s
~"); }\n";
78 res
~= "ref LayJustify set"~s
~" () pure nothrow @safe @nogc { mode = Justify."~s
~"; return this; }\n";
82 bool opEquals() (in auto ref LayJustify s
) const pure nothrow @safe @nogc { pragma(inline
, true); return (mode
== s
.mode
&& leftpad
== s
.leftpad
); }
86 // ////////////////////////////////////////////////////////////////////////// //
87 // this needs such fonts in stash:
91 // textz -- italic and bold
95 // monoz -- italic and bold
96 final class LayoutFonts
{
102 int lastSize
= -1; // to ensure that first call to `setFont()` will do it's work
107 // create new fontstash
108 FONSparams fontParams
;
109 fontParams
.width
= 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
110 fontParams
.height
= 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
111 fontParams
.flags
= FONS_ZERO_TOPLEFT
;
112 fs
= fonsCreateInternal(&fontParams
);
113 if (fs
is null) throw new Exception("error creating font stash");
114 killFontStash
= true;
115 fs
.fonsResetAtlas(1024, 1024);
116 fonsSetSpacing(fs
, 0);
118 fonsSetAlign(fs
, NVGAlign
.Left|NVGAlign
.Baseline
);
121 this (FONScontext
* afs
) {
122 if (afs
is null) assert(0, "empty font stash");
124 killFontStash
= false;
127 ~this () { freeFontStash(); }
129 void freeFontStash () {
130 if (killFontStash
&& fs
!is null) {
131 fs
.fonsDeleteInternal();
133 killFontStash
= false;
137 void setFont (int size
) { setFont(size
, lastStyle
); }
138 void setFont (LayStyle style
) { setFont(lastSize
, style
); }
140 void setFont (int size
, LayStyle style
) {
141 if (size
< 1) size
= 1;
142 if (style
!= lastStyle
) {
145 fname
[0..4] = (style
.mono ?
"mono" : "text");
146 if (style
.italic
&& style
.bold
) fname
.ptr
[fnlen
++] = 'z';
147 else if (style
.italic
) fname
.ptr
[fnlen
++] = 'i';
148 else if (style
.bold
) fname
.ptr
[fnlen
++] = 'b';
149 auto fidx
= fonsGetFontByName(fs
, fname
[0..fnlen
]);
150 if (fidx
== FONS_INVALID
) throw new Exception("font '"~fname
[0..fnlen
].idup
~"' not found");
151 fonsSetFont(fs
, fidx
);
153 fonsSetSize(fs
, size
);
158 int textWidth(T
) (const(T
)[] str) if (is(T
== char) ||
is(T
== dchar)) {
159 import std
.algorithm
: max
;
160 import core
.stdc
.math
: lrintf
;
162 float adv
= fs
.fonsTextBounds(0, 0, str, b
[]);
164 return lrintf(max(adv
, w
));
167 void textWidth2(T
) (const(T
)[] str, int* w
=null, int* wsp
=null) if (is(T
== char) ||
is(T
== dchar)) {
168 import core
.stdc
.math
: lrintf
;
169 import std
.algorithm
: max
;
170 float[4] b
= void, bsp
= void;
171 auto it
= FonsTextBoundsIterator(fs
, 0, 0);
173 float adv
= it
.advance
;
176 float advsp
= it
.advance
;
179 if (w
!is null) *w
= lrintf(max(adv
, b
[2]-b
[0]));
180 if (wsp
!is null) *wsp
= lrintf(max(advsp
, bsp
[2]-bsp
[0]));
184 import core
.stdc
.math
: lrintf
;
185 // use line bounds for height
186 float y0
= void, y1
= void;
187 fs
.fonsLineBounds(0, &y0
, &y1
);
188 return lrintf(y1
-y0
);
191 void textMetrics (int* asc
, int* desc
, int* lineh
) {
192 import core
.stdc
.math
: lrintf
;
193 float a
= void, d
= void, h
= void;
194 fs
.fonsVertMetrics(&a
, &d
, &h
);
195 if (asc
!is null) *asc
= lrintf(a
);
196 if (desc
!is null) *desc
= lrintf(d
);
197 if (lineh
!is null) *lineh
= lrintf(h
);
202 // ////////////////////////////////////////////////////////////////////////// //
203 // layouted text word
204 final class LayWord
{
208 LayText owner
; // we will take word text from it
210 bool wbreak
; // can we break line on this word?
211 bool wspaced
; // should we add spacing after this word?
212 bool wspacedOrig
; // should we add spacing after this word? original value
214 int fsize
; // font size
215 // calculated properties
216 int x
=0, w
=0, wsp
=0; // starting x position, width, width with right space
217 int h
=0, asc
=0, desc
=0; // height, ascent (positive), descent (negative)
219 // return word spacing (if any)
220 @property int spacing () const pure nothrow @safe @nogc { pragma(inline
, true); return (wspaced ? wsp
-w
: 0); }
221 @property const(dchar)[] text () const pure nothrow @safe @nogc { pragma(inline
, true); return (owner
!is null ? owner
.fulltext
[wstart
..wend
] : ""d
); }
225 // ////////////////////////////////////////////////////////////////////////// //
226 // layouted text line
227 final class LayLine
{
230 // calculated properties
231 int x
=0, y
=0, w
=0; // starting x and y positions, width
232 // on finish, layouter will calculate minimal ('cause it is negative) descent
233 int h
=0, desc
=0; // height, descent (negative)
234 ulong firstWordNum
= 0;
238 // ////////////////////////////////////////////////////////////////////////// //
240 final class LayText
{
242 // special control characters
243 enum dchar NextLineCh
= 0x2028; // 0x0085 is treated like whitespace
244 enum dchar NextParaCh
= 0x2029;
247 PropFontCh
= 0x02, // proportional
254 // the following chars implies new paragraph if previous mode is different
258 StanzaCh
= 0x0d, // new poem stanze
259 VerseCh
= 0x0e, // new stanza line
263 enum paraIndent
= " ";
267 bool lineCreated
; // is current line created? we do lazy line creation
268 ulong curWordNum
= 0;
269 bool firstParaLine
= true;
271 usize lastWordStart
; // in fulltext
272 dchar curTextMode
= NormalTextCh
;
273 bool normJustified
; // is normal text justified?
279 int width
; // maximum text width
282 // current attributes
283 int fsize
= 24; // font size
288 // calculated after finalization
289 int textHeight
= 0, textWidth
= 0;
292 // compare function should return (roughly): key-l
293 alias CmpFn
= int delegate (LayLine l
) nothrow @nogc;
295 int findLineBinary (scope CmpFn cmpfn
) {
296 if (lines
.length
== 0) return -1;
297 int bot
= 0, i
= cast(int)lines
.length
-1;
299 int mid
= i
-(i
-bot
)/2;
300 int cmp = cmpfn(lines
[mid
]);
301 if (cmp < 0) i
= mid
-1;
302 else if (cmp > 0) bot
= mid
;
305 return (cmpfn(lines
[i
]) == 0 ? i
: -1);
309 // this is for rendering engine
310 ulong[] sections
; // line at which each section starts
313 // find line with this word index
314 int findLineWithWord (ulong idx
) {
315 return findLineBinary((LayLine l
) {
316 if (idx
< l
.firstWordNum
) return -1;
317 if (idx
>= l
.firstWordNum
+l
.words
.length
) return 1;
319 if (l
.words
.length
== 0) return -1;
324 // find line which contains this coordinate
325 int findLineWithY (int y
) {
326 if (lines
.length
== 0) return 0;
328 if (y
>= textHeight
) return cast(int)lines
.length
-1;
329 auto res
= findLineBinary((LayLine l
) {
330 if (y
< l
.y
) return -1;
331 if (y
>= l
.y
+l
.h
) return 1;
334 //if (res == -1) { import std.stdio; writeln("*** y=", y, "; th=", textHeight); }
344 this (LayoutFonts alaf
, int awidth
) {
345 if (alaf
is null) assert(0, "no layout fonts");
346 if (awidth
< 1) awidth
= 1;
348 laf
.setFont(fsize
, style
);
352 // should normal text be justified? will take effect at next paragraph
353 @property bool normJustify () const pure nothrow @safe @nogc { pragma(inline
, true); return newJustified
; }
354 @property void normJustify (bool v
) pure nothrow @safe @nogc { pragma(inline
, true); newJustified
= v
; }
356 @property ulong curWordIndex () const pure nothrow @safe @nogc { pragma(inline
, true); return curWordNum
; }
358 @property int size () const pure nothrow @safe @nogc { pragma(inline
, true); return fsize
; }
359 @property void size (int newsize
) {
360 if (finalized
) throw new Exception("can't add chars to finalized layout");
361 if (newsize
< 1) newsize
= 1;
362 if (newsize
!= fsize
) {
368 // add text to layouter; it recognizes special chars
369 void put(T
) (const(T
)[] str...) if (is(T
== char) ||
is(T
== dchar)) {
370 if (finalized
) throw new Exception("can't add chars to finalized layout");
371 if (str.length
== 0) return;
373 dchar curCh
; // 0: no more chars
376 static if (is(T
== char)) {
378 if (!lastWasUtf
) { lastWasUtf
= true; dec.reset
; }
379 void skipCh () @trusted {
380 while (stpos
< str.length
) {
381 curCh
= dec.decode(cast(ubyte)str.ptr
[stpos
++]);
382 if (curCh
<= dchar.max
) return;
390 void skipCh () @trusted {
391 if (stpos
< str.length
) {
392 curCh
= str.ptr
[stpos
++];
393 if (curCh
> dchar.max
) curCh
= '?';
401 if (!dec.complete
) curCh
= '?'; else skipCh();
407 template StyleSwitchMixin(string ch
, string prop
, bool val
) {
408 static if (val
) enum sval
= "true"; else enum sval
= "false";
409 enum StyleSwitchMixin
= "case "~ch
~": if (style."~prop
~" != "~sval
~") { flushWord(); style."~prop
~" = "~sval
~"; } break;";
412 // process stream dchars
413 if (curCh
== 0) return;
415 import std
.uni
: isWhite
;
425 if (curTextMode
!= ch
) {
434 if (lineCreated
) flushLine(); else newLine();
441 mixin(StyleSwitchMixin
!("MonoFontCh", "mono", true));
442 mixin(StyleSwitchMixin
!("PropFontCh", "mono", false));
443 mixin(StyleSwitchMixin
!("ItalicOnCh", "italic", true));
444 mixin(StyleSwitchMixin
!("ItalicOffCh", "italic", false));
445 mixin(StyleSwitchMixin
!("BoldOnCh", "bold", true));
446 mixin(StyleSwitchMixin
!("BoldOffCh", "bold", false));
447 mixin(StyleSwitchMixin
!("StrikeOnCh", "strike", true));
448 mixin(StyleSwitchMixin
!("StrikeOffCh", "strike", false));
449 case 0x00a0: // non-breaking space
453 if (ch
<= ' ' ||
isWhite(ch
)) {
455 addSpace(); // this will fix the last word in the current line
466 import std
.algorithm
: max
, min
;
467 if (finalized
) throw new Exception("can't finalize already finalized layout");
470 // calculate line positions and line widthes
474 ulong nlws
= 0; // next line starting word index
475 //{ import std.stdio; writeln(lines.length, " lines"); }
476 foreach (immutable lidx
, LayLine ln
; lines
) {
477 ln
.firstWordNum
= nlws
;
478 if (ln
.words
.length
) {
479 assert(ln
.words
.ptr
[0].wordNum
== nlws
);
480 nlws
+= ln
.words
.length
;
483 if (ln
.words
.length
) {
484 // calculate line height
488 foreach (LayWord w
; ln
.words
) {
489 ln
.w
+= (w
.wspaced ? w
.wsp
: w
.w
);
490 ln
.h
= max(ln
.h
, w
.h
);
491 ln
.desc
= min(ln
.desc
, w
.desc
);
493 // calculate words coordinates
494 if (ln
.just
.justify
) {
496 int wcount
; // we don't have to space non-breaking words
497 // last word in line is marked as "nonbreaking", so we don't need to check for it
498 foreach (immutable idx
, LayWord w
; ln
.words
) {
499 if (w
.wspaced
) ++wcount
;
502 version(laytest
) { import std
.stdio
; writeln("+++ width=", width
, "; spc=", spc
, "; wcount=", wcount
); }
503 if (spc
< 0) spc
= 0;
507 frac
= spc
-xadvsp
*wcount
;
509 version(laytest
) { import std
.stdio
; writeln("+++ xspc=", spc
); }
512 foreach (LayWord w
; ln
.words
) {
515 if (wcount
&& w
.wspaced
) {
521 version(laytest
) { import std
.stdio
; writeln(" xe=", x
, "; w=", ln
.w
, "; width=", width
); }
524 if (ln
.just
.left
) x
= 0;
525 else if (ln
.just
.right
) x
= width
-ln
.w
;
526 else if (ln
.just
.center
) x
= (width
-ln
.w
)/2;
527 else assert(0, "wtf?!");
529 foreach (LayWord w
; ln
.words
) {
531 x
+= (w
.wspaced ? w
.wsp
: w
.w
);
536 if (ln
.h
<= 0) ln
.h
= 24; // just in case
541 writeln("LINE #", lidx
, "; y=", ln
.y
, "; w=", ln
.w
, "; h=", ln
.h
);
542 foreach (immutable widx
, LayWord w
; ln
.words
) {
543 import std
.conv
: to
;
544 writeln(" word #", widx
, ": x=", w
.x
, "; w=", w
.w
, "; wsp=", w
.wsp
, "; [", w
.text
.to
!string
.recodeToKOI8
, "]");
548 textWidth
= max(textWidth
, ln
.w
);
555 // word should be already flushed
557 if (!lineCreated
) return;
558 auto ln
= lines
[$-1];
559 if (ln
.words
.length
== 0) return;
560 with (ln
.words
[$-1]) { wbreak
= true; wspaced
= true; }
563 void fixParaStyle () {
564 switch (curTextMode
) {
566 if (normJustified
) just
.setJustify
; else just
.setLeft
;
567 style
.italic
= false;
569 style
.strike
= false;
570 style
.underline
= false;
574 if (normJustified
) just
.setJustify
; else just
.setLeft
;
577 style
.strike
= false;
578 style
.underline
= false;
585 style
.strike
= false;
586 style
.underline
= false;
593 style
.strike
= false;
594 style
.underline
= false;
601 style
.strike
= false;
602 style
.underline
= false;
607 style
.italic
= false;
609 style
.strike
= false;
610 style
.underline
= false;
613 default: assert(0, "wtf?!");
617 // end this paragraph; word should be already flushed
618 void flushPara (dchar newMode
=0) {
619 // last paragraph line should not be justified
621 firstParaLine
= true;
622 normJustified
= newJustified
;
623 if (newMode
) curTextMode
= newMode
;
625 if (curTextMode
== StanzaCh
) newLine();
628 // word should be flushed
631 auto ln
= new LayLine();
632 laf
.setFont(fsize
, style
);
633 ln
.h
= laf
.textHeight();
638 // paragraph should be flushed
642 auto ln
= new LayLine();
643 laf
.setFont(fsize
, style
);
644 ln
.h
= laf
.textHeight();
646 firstParaLine
= true;
649 void flushLine(string mode
="") () if (mode
== "" || mode
== "last") {
650 if (!lineCreated
) return;
651 auto ln
= lines
[$-1];
652 // last paragraph line should not be justified
653 static if (mode
== "last") {
654 if (normJustified
) ln
.just
.setJustify
; else ln
.just
.setLeft
;
656 // last word should not have space after it
657 if (ln
.words
.length
) {
658 auto w
= ln
.words
[$-1];
659 w
.wspacedOrig
= w
.wspaced
;
665 // add current word (if any) to line
666 // the word is non-spaced and non-breaking by default
668 static bool isDash (dchar ch
) {
669 pragma(inline
, true);
670 return (ch
== '-' || ch
== 0x2013);
673 while (lastWordStart
< fulltext
.length
) {
674 // break after dashes
675 bool allowBreak
= false;
676 usize wend
= lastWordStart
;
677 while (wend
< fulltext
.length
) {
678 if (isDash(fulltext
.ptr
[wend
])) {
679 while (wend
< fulltext
.length
&& isDash(fulltext
.ptr
[wend
])) ++wend
;
685 flushWordImpl(lastWordStart
, wend
, allowBreak
);
686 lastWordStart
= wend
;
690 // really add word ;-)
691 void flushWordImpl (usize wstart
, usize wend
, bool allowBreak
) {
692 import std
.algorithm
: max
;
693 assert(wend
> wstart
);
695 import std
.conv
: to
;
697 writeln("*** ", str.to
!string
.recodeToKOI8
, "|", allowBreak
);
702 // add new line if necessary
703 if (lines
.length
>= int.max
/2-8) throw new Exception("too many lines");
705 laf
.setFont(fsize
, style
);
706 ln
.h
= laf
.textHeight();
714 auto w
= new LayWord();
718 w
.wordNum
= curWordNum
++;
719 w
.wbreak
= allowBreak
;
723 // calculate dimensions
724 laf
.setFont(fsize
, style
);
725 laf
.textWidth2(w
.text
, &w
.w
, &w
.wsp
); // normal and with space
726 // calculate ascent, descent and height
727 laf
.textMetrics(&w
.asc
, &w
.desc
, &w
.h
);
728 version(laytest
) { import std
.stdio
; writeln("width=", width
, "; ln.w=", ln
.w
, "; w.w=", w
.w
, "; w.wsp=", w
.wsp
); }
729 if (ln
.words
.length
> 0) {
730 firstParaLine
= false; // just in case
731 // does this word fit?
732 int newW
= ln
.w
+ln
.words
[$-1].spacing
+w
.w
;
735 if (ln
.words
.length
>= int.max
/2) throw new Exception("too many words in line");
737 ln
.w
= newW
; // fix current line width
740 // oops, we have to start new line here
743 if (lines
.length
>= int.max
/2-8) throw new Exception("too many lines");
750 if (firstParaLine
&& (just
.left || just
.justify
)) {
752 int ind
= laf
.textWidth(paraIndent
);
753 switch (curTextMode
) {
769 firstParaLine
= false;
772 ln
.h
= max(ln
.h
, w
.h
);