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
;
35 // ////////////////////////////////////////////////////////////////////////// //
36 // this needs such fonts in stash:
40 // textz -- italic and bold
41 final class LayoutFonts
{
51 // create new fontstash
52 FONSparams fontParams
;
53 fontParams
.width
= 512/*NVG_INIT_FONTIMAGE_SIZE*/;
54 fontParams
.height
= 512/*NVG_INIT_FONTIMAGE_SIZE*/;
55 fontParams
.flags
= FONS_ZERO_TOPLEFT
;
56 fs
= fonsCreateInternal(&fontParams
);
57 if (fs
is null) throw new Exception("error creating font stash");
59 fs
.fonsResetAtlas(512, 512);
62 this (FONScontext
* afs
) {
63 if (afs
is null) assert(0, "empty font stash");
65 killFontStash
= false;
68 ~this () { freeFontStash(); }
70 void freeFontStash () {
71 if (killFontStash
&& fs
!is null) {
72 fs
.fonsDeleteInternal();
74 killFontStash
= false;
78 void setFont (float size
, bool bold
, bool italic
) {
79 string fname
= "text";
80 if (bold
&& italic
) fname
= "textz";
81 else if (bold
) fname
= "textb";
82 else if (italic
) fname
= "texti";
83 if (lastFont
!= fname
) {
84 auto fidx
= fonsGetFontByName(fs
, fname
);
85 if (fidx
== FONS_INVALID
) throw new Exception("font '"~fname
~"' not found");
86 fonsSetFont(fs
, fidx
);
89 fonsSetSize(fs
, size
);
90 fonsSetSpacing(fs
, 0);
92 fonsSetAlign(fs
, NVGAlign
.Left|NVGAlign
.Top
);
96 float textBounds(T
) (const(T
)[] str, float* w
=null, float* h
=null) if (is(T
== char) ||
is(T
== dchar)) {
98 float adv
= fs
.fonsTextBounds(0, 0, str, b
[]);
99 if (w
!is null || h
!is null) {
100 // use line bounds for height
101 fs
.fonsLineBounds(0, &b
[1], &b
[3]);
102 if (w
!is null) *w
= b
[2]-b
[0];
103 if (h
!is null) *h
= b
[3]-b
[1];
107 auto it = FonsTextBoundsIterator(fs, 0, 0);
115 // use line bounds for height
116 fs.fonsLineBounds(0*scale, &bn[1], &bn[3]);
119 auto adv = fs.fonsTextBounds(0, 0, "Woo", bn[]);
127 // ////////////////////////////////////////////////////////////////////////// //
128 // layouted text word
129 final class LayWord
{
131 bool wbreak
; // can we break line on this word?
132 bool italic
, bold
; // attrs
133 float size
; // font size
134 // calculated properties
135 float x
=0, w
=0, h
=0, wsp
=0; // starting x position, width, height, width with right space
140 // ////////////////////////////////////////////////////////////////////////// //
141 // layouted text line
142 final class LayLine
{
143 enum Justify
{ Left
, Right
, Center
, Justify
}
145 //float padl=0, padr=0, padt=0, padb=0; // padding
146 Justify just
= Justify
.Left
;
147 // calculated properties
148 float x
=0, y
=0, w
=0, h
=0; // starting x and y positions, width, height
152 // ////////////////////////////////////////////////////////////////////////// //
154 final class LayText
{
157 bool lineUsed
; // is current line used? note that it can be used even if it has no words at all
158 bool oldItalic
, oldBold
;
160 float lastSz
; // to fill empty line's height
161 ulong curWordNum
= 0;
162 bool firstParaLine
= true;
164 void fixFont (float sz
, bool i
, bool b
) {
165 if (sz
!= oldSize || i
!= oldItalic || b
!= oldBold
) {
166 laf
.setFont(sz
, i
, b
);
176 float width
; // maximum text width
177 // current attributes
178 float size
= 24; // font size
179 LayLine
.Justify just
= LayLine
.Justify
.Left
;
181 // calculated after finalization
182 float textHeight
= 0, textWidth
= 0;
184 this (LayoutFonts alaf
, float awidth
) {
185 if (alaf
is null) assert(0, "no layout fonts");
188 laf
.setFont(size
, italic
, bold
);
195 @property ulong curWordIndex () const pure nothrow @safe @nogc { return curWordNum
; }
197 // break current line
198 void breakLine () { breakLine(size
, just
); }
200 // break current line, set new line attributes
201 void breakLine (LayLine
.Justify newjust
) { breakLine(size
, newjust
); }
203 // break current line, set new line attributes
204 void breakLine (float newsize
) { breakLine(newsize
, just
); }
206 // break current line, set new line attributes
207 void breakLine (float newsize
, LayLine
.Justify newjust
) {
208 if (finalized
) throw new Exception("can't add lines to finalized layout");
209 // last paragraph line should not be justified
210 if (lineUsed
&& lines
[$-1].just
== LayLine
.Justify
.Justify
) lines
[$-1].just
= LayLine
.Justify
.Left
;
214 firstParaLine
= true;
218 if (finalized
) throw new Exception("can't add lines to finalized layout");
219 // last paragraph line should not be justified
220 if (lineUsed
&& lines
[$-1].just
== LayLine
.Justify
.Justify
) lines
[$-1].just
= LayLine
.Justify
.Left
;
221 finishLine(); // finish current line
222 lines
~= new LayLine();
223 fixFont(size
, italic
, bold
);
224 laf
.textBounds(" ", null, &lines
[$-1].h
);
225 lines
[$-1].just
= just
;
228 firstParaLine
= true;
231 // add word to current line, with wrapping
232 void addWord(T
) (T
str, bool allowBreak
=true) if (is(T
: const(char)[]) ||
is(T
: const(dchar)[])) {
233 if (finalized
) throw new Exception("can't add words to finalized layout");
234 static if (is(T
== typeof(null))) {
235 addWord("", allowBreak
); // eh...
236 } else static if (is(T
: const(char)[])) {
237 // convert to dstring
242 foreach (char ch
; str) {
243 dchar dch
= dec.decode(ch
);
244 if (dch
<= dchar.max
) {
245 if (len
>= int.max
) throw new Exception("word too long");
252 ds = new dchar[](len
);
255 foreach (char ch
; str) {
256 dchar dch
= dec.decode(ch
);
257 if (dch
<= dchar.max
) ds.ptr
[len
++] = dch
;
260 addWord(cast(dstring
)ds, allowBreak
); // it is safe to cast here
262 // we have dchar input
264 import std
.conv
: to
;
266 writeln("*** ", str.to
!string
.recodeToKOI8
, "|", allowBreak
);
269 if (str.length
== 0) {
270 // this is empty word, convert previous to breaking word if necessary
271 if (!allowBreak
) return; // nothing to do
272 if (!lineUsed
) return; // nothing to do
273 auto ln
= lines
[$-1];
274 if (ln
.words
.length
== 0) return; // nothing to do
275 //if (ln.words[$-1].wbreak) return;
276 //ln.w += (ln.words[$-1].wsp-ln.words[$-1].w); // plus previous' word space
277 ln
.words
[$-1].wbreak
= true; // convert to breaking
282 // add new line if necessary
283 lines
~= new LayLine();
284 fixFont(size
, italic
, bold
);
285 laf
.textBounds(" ", null, &lines
[$-1].h
);
286 lines
[$-1].just
= just
;
289 auto ln
= lines
[$-1];
291 auto w
= new LayWord();
292 static if (is(T
== dstring
)) w
.text
= str; else w
.text
= str.idup
;
293 w
.wordnum
= curWordNum
++;
294 w
.wbreak
= allowBreak
;
298 // calculate dimensions
299 fixFont(size
, italic
, bold
);
300 laf
.textBounds(w
.text
, &w
.w
, &w
.h
);
302 w
.wsp
= w
.w
+laf
.textBounds(" ");
303 version(laytest
) { import std
.stdio
; writeln("width=", width
, "; ln.w=", ln
.w
, "; w.w=", w
.w
, "; w.wsp=", w
.wsp
); }
304 if (ln
.words
.length
> 0) {
306 // does this word fit?
307 float newW
= ln
.w
+w
.w
;
308 if (ln
.words
[$-1].wbreak
) newW
+= (ln
.words
[$-1].wsp
-ln
.words
[$-1].w
); // plus previous' word space
311 if (ln
.words
.length
>= int.max
/2) throw new Exception("too many words in line");
316 // oops, we have to start new line here
319 lines
~= new LayLine();
320 fixFont(size
, italic
, bold
);
321 laf
.textBounds(" ", null, &lines
[$-1].h
);
322 lines
[$-1].just
= just
;
326 if (firstParaLine
&& (just
== LayLine
.Justify
.Left || just
== LayLine
.Justify
.Justify
)) {
328 float ind
= laf
.textBounds(" ");
333 firstParaLine
= false;
342 import std
.algorithm
: max
;
343 if (finalized
) throw new Exception("can't finalize already finalized layout");
344 // last paragraph line should not be justified
345 if (lineUsed
&& lines
[$-1].just
== LayLine
.Justify
.Justify
) lines
[$-1].just
= LayLine
.Justify
.Left
;
348 // calculate line positions and line widthes
352 //{ import std.stdio; writeln(lines.length, " lines"); }
353 foreach (immutable lidx
, LayLine ln
; lines
) {
355 if (ln
.words
.length
) {
356 // calculate line height
358 foreach (LayWord w
; ln
.words
) ln
.h
= max(ln
.h
, w
.h
);
359 // calculate words coordinates
360 if (ln
.just
== LayLine
.Justify
.Justify
) {
362 int wcount
; // we don't have to space non-breaking words
363 foreach (immutable idx
, LayWord w
; ln
.words
) {
364 if (w
.wbreak
/*&& idx < ln.words.length*/) ++wcount
;
367 version(laytest
) { import std
.stdio
; writeln("+++ width=", width
, "; spc=", spc
, "; wcount=", wcount
); }
368 if (spc
< 0) spc
= 0;
369 if (wcount
> 0) spc
/= wcount
;
370 version(laytest
) { import std
.stdio
; writeln("+++ xspc=", spc
); }
373 foreach (LayWord w
; ln
.words
) {
374 w
.x
= cast(int)(w
.x
+x
+0.5);
376 if (w
.wbreak
) x
+= spc
;
379 version(laytest
) { import std
.stdio
; writeln(" xe=", x
, "; w=", ln
.w
, "; width=", width
); }
382 final switch (ln
.just
) {
383 case LayLine
.Justify
.Left
: x
= 0; break;
384 case LayLine
.Justify
.Right
: x
= width
-ln
.w
; break;
385 case LayLine
.Justify
.Center
: x
= (width
-ln
.w
)/2; break;
386 case LayLine
.Justify
.Justify
: assert(0, "wtf?!");
389 foreach (LayWord w
; ln
.words
) {
391 // if (ln.just == LayLine.Justify.Center) w.x -= w.w/2;
392 //else if (ln.just == LayLine.Justify.Right) w.x -= w.w;
393 x
+= (w
.wbreak ? w
.wsp
: w
.w
);
398 if (ln
.h
<= 0) ln
.h
= 24; // just in case
403 writeln("LINE #", lidx
, "; y=", ln
.y
, "; w=", ln
.w
, "; h=", ln
.h
);
404 foreach (immutable widx
, LayWord w
; ln
.words
) {
405 import std
.conv
: to
;
406 writeln(" word #", widx
, ": x=", w
.x
, "; w=", w
.w
, "; wsp=", w
.wsp
, "; [", w
.text
.to
!string
.recodeToKOI8
, "]");
416 // finish current line
418 if (!lineUsed
) return;
420 if (lines[$-1].h <= 0) {
421 //if (lines[$-1].words.length) { import std.stdio; foreach (LayWord w; lines[$-1].words) write(" ", w.text); writeln("|"); }
422 //assert(lines[$-1].words.length == 0);
423 fixFont(lastSz, false, false);
424 laf.textBounds(" ", null, &lines[$-1].h);
427 // last word should not have space after it
428 if (lines
[$-1].words
.length
) {
429 auto w
= lines
[$-1].words
[$-1];
438 // ////////////////////////////////////////////////////////////////////////// //
441 auto laf = new LayoutFonts();
442 laf.fs.fonsAddFont("text:noaa", "/home/ketmar/ttf/ms/verdana.ttf");
443 laf.fs.fonsAddFont("textb:noaa", "/home/ketmar/ttf/ms/verdanab.ttf");
444 laf.fs.fonsAddFont("texti:noaa", "/home/ketmar/ttf/ms/verdanai.ttf");
445 laf.fs.fonsAddFont("textz:noaa", "/home/ketmar/ttf/ms/verdanaz.ttf");
447 auto lay = new LayText(laf, 900-4-2-BND_SCROLLBAR_WIDTH-2);
454 //enum fname = "/home/ketmar/back/D/prj/xreader/_boox/Vorobev_Nabla-kvadrat.318494.fb2.zip";
455 enum fname = "/home/ketmar/back/D/prj/xreader/_boox/Rozov_Konfederaciya-Meganeziya_5_Drayv-Astarty.244381.fb2.zip";
456 auto stt = MonoTime.currTime;
457 book = new BookText(fname);
458 writeln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds");
463 auto stt = MonoTime.currTime;
464 foreach (immutable sidx, BookText.Section sc; book.secs) {
467 foreach (BookText.Line tp; sc.title) {
468 lay.breakLine(LayLine.Justify.Center);
469 foreach (immutable widx, dstring w; tp.words) lay.addWord(w, tp.needspace[widx]);
471 foreach (BookText.Line ep; sc.epi) {
472 lay.breakLine(LayLine.Justify.Right);
474 foreach (immutable widx, dstring w; ep.words) lay.addWord(w, ep.needspace[widx]);
477 foreach (immutable pidx, BookText.Line ps; sc.pars) {
478 lay.breakLine(LayLine.Justify.Left);
479 foreach (immutable widx, dstring w; ps.words) lay.addWord(w, ps.needspace[widx]);
483 writeln("layouted in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds");