layouter seems to work now
[xreader.git] / xlayouter.d
blob401c139b1677e48fa85c3a9d78ef88001505c8d8
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/>.
17 module xlayouter;
19 import core.time;
20 import std.stdio;
22 import arsd.color;
23 import arsd.png;
24 import arsd.jpeg;
26 import iv.nanovg;
27 import iv.nanovg.oui.blendish;
28 import iv.utfutil;
30 import booktext;
32 version(laytest) import iv.encoding;
35 // ////////////////////////////////////////////////////////////////////////// //
36 // this needs such fonts in stash:
37 // text -- normal
38 // texti -- italic
39 // textb -- bold
40 // textz -- italic and bold
41 final class LayoutFonts {
42 public:
43 FONScontext* fs;
45 private:
46 bool killFontStash;
47 string lastFont;
49 public:
50 this () {
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");
58 killFontStash = true;
59 fs.fonsResetAtlas(512, 512);
62 this (FONScontext* afs) {
63 if (afs is null) assert(0, "empty font stash");
64 fs = afs;
65 killFontStash = false;
68 ~this () { freeFontStash(); }
70 void freeFontStash () {
71 if (killFontStash && fs !is null) {
72 fs.fonsDeleteInternal();
74 killFontStash = false;
75 fs = null;
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);
87 lastFont = fname;
89 fonsSetSize(fs, size);
90 fonsSetSpacing(fs, 0);
91 fonsSetBlur(fs, 0);
92 fonsSetAlign(fs, NVGAlign.Left|NVGAlign.Top);
95 // returns advance
96 float textBounds(T) (const(T)[] str, float* w=null, float* h=null) if (is(T == char) || is(T == dchar)) {
97 float[4] b = void;
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];
105 return adv;
107 auto it = FonsTextBoundsIterator(fs, 0, 0);
108 assert(it.valid);
109 it.put("Woo");
110 writeln(it.advance);
111 float[4] bn;
112 it.getBounds(bn[]);
113 writeln(bn[]);
115 // use line bounds for height
116 fs.fonsLineBounds(0*scale, &bn[1], &bn[3]);
117 writeln(bn[]);
119 auto adv = fs.fonsTextBounds(0, 0, "Woo", bn[]);
120 writeln(adv);
121 writeln(bn[]);
127 // ////////////////////////////////////////////////////////////////////////// //
128 // layouted text word
129 final class LayWord {
130 dstring text;
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
136 ulong wordnum;
140 // ////////////////////////////////////////////////////////////////////////// //
141 // layouted text line
142 final class LayLine {
143 enum Justify { Left, Right, Center, Justify }
144 LayWord[] words;
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 // ////////////////////////////////////////////////////////////////////////// //
153 // layouted text
154 final class LayText {
155 private:
156 bool finalized;
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;
159 float oldSize;
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);
167 oldSize = sz;
168 oldItalic = i;
169 oldBold = b;
173 public:
174 LayoutFonts laf;
175 LayLine[] lines;
176 float width; // maximum text width
177 // current attributes
178 float size = 24; // font size
179 LayLine.Justify just = LayLine.Justify.Left;
180 bool italic, bold;
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");
186 laf = alaf;
187 width = awidth;
188 laf.setFont(size, italic, bold);
189 oldSize = size;
190 oldItalic = italic;
191 oldBold = bold;
192 lastSz = size;
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;
211 finishLine();
212 size = newsize;
213 just = newjust;
214 firstParaLine = true;
217 void emptyLine () {
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;
226 lineUsed = true;
227 finishLine();
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
238 dchar[] ds;
239 uint len = 0;
240 { // calc length
241 Utf8Decoder dec;
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");
246 ++len;
250 assert(len);
251 { // convert
252 ds = new dchar[](len);
253 Utf8Decoder dec;
254 len = 0;
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
261 } else {
262 // we have dchar input
263 version(laytest) {
264 import std.conv : to;
265 import std.stdio;
266 writeln("*** ", str.to!string.recodeToKOI8, "|", allowBreak);
268 lastSz = size;
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
278 return;
280 // normal word
281 if (!lineUsed) {
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;
287 lineUsed = true;
289 auto ln = lines[$-1];
290 // create word
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;
295 w.italic = italic;
296 w.bold = bold;
297 w.size = size;
298 // calculate dimensions
299 fixFont(size, italic, bold);
300 laf.textBounds(w.text, &w.w, &w.h);
301 //assert(w.h > 0);
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) {
305 //assert(ln.h > 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
309 if (newW <= width) {
310 // yay, it fits!
311 if (ln.words.length >= int.max/2) throw new Exception("too many words in line");
312 ln.words ~= w;
313 ln.w = newW;
314 return;
316 // oops, we have to start new line here
317 finishLine();
318 // add new line
319 lines ~= new LayLine();
320 fixFont(size, italic, bold);
321 laf.textBounds(" ", null, &lines[$-1].h);
322 lines[$-1].just = just;
323 lineUsed = true;
325 ln = lines[$-1];
326 if (firstParaLine && (just == LayLine.Justify.Left || just == LayLine.Justify.Justify)) {
327 // paragraph indent
328 float ind = laf.textBounds(" ");
329 w.w += ind;
330 w.wsp += ind;
331 w.x = ind; // indent
333 firstParaLine = false;
334 ln.words ~= w;
335 ln.w = w.w;
336 ln.h = w.h;
340 // finalize layout
341 void finalize () {
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;
346 finishLine();
347 finalized = true;
348 // calculate line positions and line widthes
349 textHeight = 0;
350 textWidth = 0;
351 float curY = 0;
352 //{ import std.stdio; writeln(lines.length, " lines"); }
353 foreach (immutable lidx, LayLine ln; lines) {
354 ln.y = curY;
355 if (ln.words.length) {
356 // calculate line height
357 ln.h = 0;
358 foreach (LayWord w; ln.words) ln.h = max(ln.h, w.h);
359 // calculate words coordinates
360 if (ln.just == LayLine.Justify.Justify) {
361 float spc = width;
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;
365 spc -= w.w;
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); }
371 float x = 0;
372 ln.x = x;
373 foreach (LayWord w; ln.words) {
374 w.x = cast(int)(w.x+x+0.5);
375 x += w.w;
376 if (w.wbreak) x += spc;
378 ln.w = x;
379 version(laytest) { import std.stdio; writeln(" xe=", x, "; w=", ln.w, "; width=", width); }
380 } else {
381 float x = 0;
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?!");
388 ln.x = x;
389 foreach (LayWord w; ln.words) {
390 w.x += x;
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);
395 ln.w = x;
397 } else {
398 if (ln.h <= 0) ln.h = 24; // just in case
399 ln.w = 0;
401 version(laytest) {
402 import std.stdio;
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, "]");
409 curY += ln.h;
410 laf = null;
412 textHeight = curY;
415 private:
416 // finish current line
417 void finishLine () {
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];
430 w.wbreak = false;
431 w.wsp = w.w;
433 lineUsed = false;
438 // ////////////////////////////////////////////////////////////////////////// //
440 void main () {
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);
449 lay.addWord("Test");
450 lay.finalize();
452 BookText book;
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");
461 lay.breakLine(24);
463 auto stt = MonoTime.currTime;
464 foreach (immutable sidx, BookText.Section sc; book.secs) {
465 lay.italic = false;
466 lay.bold = false;
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);
473 lay.italic = true;
474 foreach (immutable widx, dstring w; ep.words) lay.addWord(w, ep.needspace[widx]);
475 lay.italic = false;
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]);
482 lay.finalize();
483 writeln("layouted in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds");