fixed incorrect text size calculation
[xreader.git] / xreaderfmt.d
blobaf0bf088685c4e9341bb1e4ee31a733f200fab6e
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 xreaderfmt;
19 import core.time;
21 import std.concurrency;
23 import iv.strex;
25 import arsd.simpledisplay;
26 import arsd.image;
28 import iv.cmdcon;
29 import iv.nanovega;
30 import iv.nanovega.blendish;
31 import iv.nanovega.textlayouter;
32 import iv.vfs;
33 import iv.vfs.io;
35 import booktext;
37 import xreadercfg;
40 // ////////////////////////////////////////////////////////////////////////// //
41 BookText loadBook (string fname) {
42 __gshared bool eliteShipsLoaded = false;
44 import std.path;
45 //import core.memory : GC;
46 fname = fname.expandTilde.absolutePath;
47 //conwriteln("loading '", fname, "'...");
48 //GC.disable();
49 auto stt = MonoTime.currTime;
50 auto book = new BookText(fname);
51 //GC.enable();
52 //GC.collect();
53 { conwriteln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds"); }
55 string[] lines;
56 string[] comments;
57 try {
58 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
59 if (line != fname) {
60 lines ~= line;
61 } else {
62 while (lines.length && lines[$-1].isComment) {
63 comments ~= lines[$-1];
64 lines = lines[0..$-1];
68 } catch (Exception) {}
69 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
70 auto fo = VFile(buildPath(RcDir, ".lastfile"), "w");
71 foreach (string s; lines) fo.writeln(s);
72 foreach_reverse (string s; comments) fo.writeln(s);
73 fo.writeln(fname);
75 stateFileName = buildPath(RcDir, fname.baseName~".rc");
77 if (!eliteShipsLoaded) {
78 loadEliteShips();
79 eliteShipsLoaded = true;
82 return book;
86 // ////////////////////////////////////////////////////////////////////////// //
87 private struct ImageInfo {
88 enum MaxIdleFrames = 5;
89 NVGImage iid;
90 TrueColorImage img;
91 int framesNotUsed;
94 private __gshared ImageInfo[] nvgImageIds;
97 void releaseImages (NVGContext vg) {
98 if (nvgImageIds.length) {
99 foreach (ref ImageInfo nfo; nvgImageIds) {
100 if (nfo.iid.valid) {
101 if (++nfo.framesNotUsed > ImageInfo.MaxIdleFrames) {
102 //conwriteln("freeing image with id ", nfo.iid);
103 vg.deleteImage(nfo.iid);
104 nfo.img = null;
112 private NVGImage registerImage (NVGContext vg, TrueColorImage img) {
113 if (vg is null || img is null) return NVGImage.init;
114 uint freeIdx = uint.max;
115 foreach (immutable idx, ref ImageInfo nfo; nvgImageIds) {
116 if (nfo.img is img) {
117 assert(nfo.iid.valid);
118 nfo.framesNotUsed = 0;
119 return nfo.iid;
121 if (freeIdx != uint.max && !nfo.iid.valid) freeIdx = cast(uint)idx;
123 NVGImage iid = vg.createImageFromMemoryImage(img); //vg.createImageRGBA(img.width, img.height, img.imageData.bytes[], NVGImageFlags.None);
124 if (!iid.valid) return NVGImage.init;
125 nvgImageIds ~= ImageInfo(iid, img, 0);
126 return iid;
130 class BookImage : LayObject {
131 BookText book;
132 uint imageidx;
133 TrueColorImage img;
134 this (BookText abook, uint aidx) { book = abook; imageidx = aidx; img = book.images[aidx].img; }
135 override int width () { return img.width; }
136 override int spacewidth () { return 0; }
137 override int height () { return img.height; }
138 override int ascent () { return img.height; }
139 override int descent () { return 0; }
140 override bool canbreak () { return true; }
141 override bool spaced () { return false; }
142 override void draw (NVGContext vg, float x, float y) {
143 y -= img.height;
144 vg.save();
145 scope(exit) vg.restore();
146 vg.beginPath();
147 NVGImage iid = vg.registerImage(img);
148 if (!iid.valid) return;
149 vg.fillPaint(vg.imagePattern(x, y, img.width, img.height, 0, iid));
150 //vg.globalAlpha(0.5);
151 vg.rect(x, y, img.width, img.height);
152 vg.fill();
157 // ////////////////////////////////////////////////////////////////////////// //
158 class BookMetadata {
159 static struct Anc {
160 dstring name;
161 uint wordidx; // first word
163 Anc[] sections;
164 Anc[uint] hrefs; // `name` is dest
165 Anc[] ids;
169 // ////////////////////////////////////////////////////////////////////////// //
170 final class FBFormatter {
171 private:
172 BookText book;
173 bool hasSections;
175 public:
176 LayTextC lay;
177 BookMetadata meta;
179 public:
180 this () {}
182 void formatBook (BookText abook, int maxWidth) {
183 assert(abook !is null);
185 lay = new LayTextC(laf, maxWidth);
186 lay.fontStyle.color = colorText.asUint;
188 meta = new BookMetadata();
190 book = abook;
191 scope(exit) book = null;
192 hasSections = false;
194 putMain();
196 lay.finalize();
199 private:
200 void dumpTree (Tag ct) {
201 while (ct !is null) {
202 conwriteln("tag: ", ct.name);
203 ct = ct.parent;
207 void badTag (Tag tag, string msg=null) {
208 import std.string : format, indexOf;
209 assert(tag !is null);
210 dumpTree(tag);
211 if (msg.length == 0) msg = "invalid tag: '%s'";
212 if (msg.indexOf("%s") >= 0) {
213 throw new Exception(msg.format(tag.name));
214 } else {
215 throw new Exception(msg);
219 void saveTagId (Tag tag) {
220 if (tag.id.length) {
221 import std.conv : to;
222 meta.ids ~= BookMetadata.Anc(tag.id.to!dstring, lay.nextWordIndex);
226 void registerSection (Tag tag) {
227 import std.conv : to;
228 string text = tag.textContent.xstrip;
229 //lay.sections ~= lay.curWordIndex;
230 string name;
231 while (text.length) {
232 char ch = text.ptr[0];
233 text = text[1..$];
234 if (ch <= ' ' || ch == 127) {
235 if (name.length == 0) continue;
236 if (ch != '\n') ch = ' ';
237 if (ch == '\n') {
238 if (name[$-1] > ' ') name ~= "\n...";
239 text = text.xstrip;
240 } else {
241 if (name[$-1] > ' ') name ~= ' ';
243 } else {
244 name ~= ch;
247 name = xstrip(name);
248 if (name.length == 0) name = "* * *";
249 meta.sections ~= BookMetadata.Anc(name.to!dstring, lay.nextWordIndex);
252 void putImage (Tag tag) {
253 if (tag is null || tag.href.length < 2 || tag.href[0] != '#') return;
254 string iid = tag.href[1..$];
255 //conwriteln("searching for image with href '", iid, "' (", book.images.length, ")");
256 foreach (immutable idx, ref BookText.Image img; book.images) {
257 if (iid == img.id) {
258 //conwriteln("image '", img.id, "' found");
259 lay.putObject(new BookImage(book, cast(uint)idx));
260 return;
265 void putOneTag(bool allowBad=false, bool allowText=true) (Tag tag) {
266 if (tag is null) return;
267 if (tag.name.length == 0) {
268 static if (allowText) {
269 saveTagId(tag);
270 lay.put(tag.text);
272 return;
274 switch (tag.name) {
275 case "p":
276 putTagContents(tag);
277 lay.endPara();
278 break;
280 case "strong":
281 lay.pushStyles();
282 scope(exit) lay.popStyles;
283 lay.fontStyle.bold = true;
284 putTagContents(tag);
285 break;
286 case "emphasis":
287 lay.pushStyles();
288 scope(exit) lay.popStyles;
289 lay.fontStyle.italic = true;
290 //fixFont();
291 putTagContents(tag);
292 break;
293 case "strikethrough":
294 lay.pushStyles();
295 scope(exit) lay.popStyles;
296 lay.fontStyle.strike = true;
297 //fixFont();
298 putTagContents(tag);
299 break;
301 case "empty-line":
302 lay.endLine();
303 break;
304 case "br":
305 lay.endPara();
306 break;
308 case "image":
309 lay.pushStyles();
310 scope(exit) lay.popStyles;
311 lay.endLine;
312 lay.lineStyle.setCenter;
313 putImage(tag);
314 lay.endLine;
315 return;
317 case "style":
318 //???
319 return;
321 case "a":
322 if (tag.href.length) {
323 import std.conv : to;
324 //conwriteln("href found: '", ct.href, "' (", lay.nextWordIndex, ")");
325 meta.hrefs[lay.nextWordIndex] = BookMetadata.Anc(tag.href.to!dstring, lay.nextWordIndex);
326 lay.pushStyles();
327 auto c = lay.fontStyle.color;
328 scope(exit) { lay.popStyles; lay.fontStyle.color = c; }
329 lay.fontStyle.href = true;
330 lay.fontStyle.underline = true;
331 lay.fontStyle.color = colorTextHref.asUint;
332 //fixFont();
333 putTagContents(tag);
334 } else {
335 putTagContents(tag);
337 break;
339 case "sub": // some idiotic files has this
340 case "sup": // some idiotic files has this
341 putTagContents(tag);
342 break;
344 case "code":
345 lay.pushStyles();
346 scope(exit) lay.popStyles;
347 setCodeStyle();
348 putTagContents(tag);
349 lay.endPara;
350 break;
352 case "poem":
353 lay.pushStyles();
354 scope(exit) lay.popStyles;
355 putPoem(tag);
356 //lay.endPara;
357 break;
359 case "stanza":
360 putTagContents(tag);
361 lay.endPara;
362 break;
364 case "v": // for stanzas
365 putTagContents(tag);
366 lay.endPara;
367 break;
369 case "date": // for poem
370 lay.endPara;
371 lay.pushStyles();
372 scope(exit) lay.popStyles;
373 lay.fontStyle.bold = true;
374 lay.fontStyle.italic = true;
375 lay.lineStyle.setRight;
376 //fixFont();
377 putTagContents(tag);
378 lay.endPara;
379 break;
381 case "cite":
382 putCite(tag);
383 return;
385 case "site":
386 case "annotation":
387 lay.pushStyles();
388 scope(exit) lay.popStyles;
389 lay.fontStyle.bold = true;
390 lay.lineStyle.setCenter;
391 //fixFont();
392 putTagContents(tag);
393 lay.endPara;
394 break;
396 case "text-author":
397 break;
399 case "title":
400 lay.pushStyles();
401 scope(exit) lay.popStyles;
402 setTitleStyle();
403 registerSection(tag);
404 putTagContents(tag);
405 if (tag.text.length == 0) lay.endPara();
406 break;
408 case "subtitle":
409 lay.pushStyles();
410 scope(exit) lay.popStyles;
411 setSubtitleStyle();
412 putTagContents(tag);
413 if (tag.text.length == 0) lay.endPara();
414 break;
416 case "epigraph":
417 lay.pushStyles();
418 scope(exit) lay.popStyles;
419 //auto olpad = lay.lineStyle.leftpad;
420 auto orpad = lay.lineStyle.rightpad;
421 setEpigraphStyle();
422 //lay.lineStyle.leftpad = lay.lineStyle.leftpad+olpad;
423 lay.lineStyle.rightpad = lay.lineStyle.rightpad+orpad;
424 putEpigraph(tag);
425 break;
427 case "section":
428 putTagContents(tag);
429 lay.endPara;
430 break;
432 case "table":
433 lay.pushStyles();
434 scope(exit) lay.popStyles;
435 lay.fontStyle.fontsize += 8;
436 lay.lineStyle.setCenter;
437 //fixFont();
438 lay.put("TABLE SKIPPED");
439 lay.endPara();
440 break;
442 default:
443 static if (allowBad) {
444 break;
445 } else {
446 badTag(tag);
451 bool onlyImagePara (Tag ct) {
452 if (ct is null) return false;
453 int count;
454 foreach (Tag tag; ct.children) {
455 if (tag.name.length == 0) {
456 // text
457 if (tag.text.xstrip.length != 0) return false;
458 continue;
460 if (count != 0 || tag.name != "image") return false;
461 ++count;
463 return (count == 1);
466 void putTagContents/*(bool xdump=false)*/ (Tag ct) {
467 if (ct is null) return;
468 saveTagId(ct);
469 if (onlyImagePara(ct)) {
470 lay.pushStyles();
471 scope(exit) lay.popStyles;
472 lay.endLine;
473 lay.lineStyle.setCenter;
474 foreach (Tag tag; ct.children) putOneTag(tag);
475 lay.endLine;
476 } else {
477 //if (ct.name == "subtitle") conwriteln("text: [", ct.children[0].text, "]");
478 foreach (Tag tag; ct.children) {
479 //static if (xdump) conwriteln("text: [", tag.text, "]");
480 putOneTag(tag);
483 //lay.endPara();
486 void putParas (Tag ct) {
487 if (ct is null) return;
488 putTagContents(ct);
489 lay.endPara();
492 void putAuthor (Tag ct) {
493 saveTagId(ct);
494 foreach (Tag tag; ct.children) {
495 if (tag.name.length == 0) continue;
496 if (tag.name == "text-author") {
497 putTagContents(tag);
498 lay.endPara();
503 void putCite (Tag ct) {
504 lay.pushStyles();
505 scope(exit) lay.popStyles;
506 setCiteStyle();
507 putParas(ct);
508 putAuthor(ct);
511 void putEpigraph (Tag ct) {
512 putParas(ct);
513 putAuthor(ct);
516 void putPoem (Tag ct) {
517 saveTagId(ct);
518 lay.pushStyles();
519 scope(exit) lay.popStyles;
520 setPoemStyle();
521 // put epigraph (not yet)
522 // put title and subtitle
523 foreach (Tag tag; ct.children) {
524 if (tag.name == "title") {
525 lay.pushStyles();
526 scope(exit) lay.popStyles;
527 if (!hasSections) registerSection(tag);
528 putParas(tag);
529 lay.endPara(); // space
530 } else if (tag.name == "subtitle") {
531 lay.pushStyles();
532 scope(exit) lay.popStyles;
533 putParas(tag);
534 //lay.endPara(); // space
537 lay.endPara;
538 foreach (Tag tag; ct.children) {
539 putOneTag!(false, false)(tag); // skip text without tags
541 putAuthor(ct);
544 void putMain () {
545 setNormalStyle();
546 foreach (Tag tag; book.content.children) {
547 putOneTag(tag);
551 private:
552 static public void fixFont (LayFontStash laf, ref LayFontStyle st) nothrow @safe @nogc {
553 if (st.monospace) {
554 if (st.italic) {
555 if (st.bold) st.fontface = laf.fontFaceId("monoz");
556 else st.fontface = laf.fontFaceId("monoi");
557 } else if (st.bold) {
558 if (st.italic) st.fontface = laf.fontFaceId("monoz");
559 else st.fontface = laf.fontFaceId("monob");
560 } else {
561 st.fontface = laf.fontFaceId("mono");
563 } else {
564 if (st.italic) {
565 if (st.bold) st.fontface = laf.fontFaceId("textz");
566 else st.fontface = laf.fontFaceId("texti");
567 } else if (st.bold) {
568 if (st.italic) st.fontface = laf.fontFaceId("textz");
569 else st.fontface = laf.fontFaceId("textb");
570 } else {
571 st.fontface = laf.fontFaceId("text");
576 void setNormalStyle () {
577 lay.fontStyle.resetAttrs;
578 lay.fontStyle.fontsize = fsizeText;
579 lay.fontStyle.color = colorText.asUint;
580 lay.lineStyle.leftpad = 0;
581 lay.lineStyle.rightpad = 0;
582 lay.lineStyle.paraIndent = 3;
583 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
584 //fixFont();
587 void setTitleStyle () {
588 lay.fontStyle.resetAttrs;
589 lay.fontStyle.bold = true;
590 lay.fontStyle.fontsize = fsizeText+4;
591 lay.fontStyle.color = colorText.asUint;
592 lay.lineStyle.leftpad = 0;
593 lay.lineStyle.rightpad = 0;
594 lay.lineStyle.paraIndent = 0;
595 lay.lineStyle.setCenter;
596 //fixFont();
599 void setSubtitleStyle () {
600 lay.fontStyle.resetAttrs;
601 lay.fontStyle.bold = true;
602 lay.fontStyle.fontsize = fsizeText+2;
603 lay.fontStyle.color = colorText.asUint;
604 lay.lineStyle.leftpad = 0;
605 lay.lineStyle.rightpad = 0;
606 lay.lineStyle.paraIndent = 0;
607 lay.lineStyle.setCenter;
608 //fixFont();
611 void setCiteStyle () {
612 lay.fontStyle.resetAttrs;
613 lay.fontStyle.italic = true;
614 lay.fontStyle.fontsize = fsizeText;
615 lay.fontStyle.color = colorText.asUint;
616 lay.lineStyle.leftpad = fsizeText*3;
617 lay.lineStyle.rightpad = fsizeText*3;
618 lay.lineStyle.paraIndent = 3;
619 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
620 //fixFont();
623 void setEpigraphStyle () {
624 lay.fontStyle.resetAttrs;
625 lay.fontStyle.italic = true;
626 lay.fontStyle.fontsize = fsizeText;
627 lay.fontStyle.color = colorText.asUint;
628 lay.lineStyle.leftpad = lay.width/3;
629 lay.lineStyle.rightpad = 0;
630 lay.lineStyle.paraIndent = 0;
631 lay.lineStyle.setRight;
632 //fixFont();
635 void setPoemStyle () {
636 lay.fontStyle.resetAttrs;
637 lay.fontStyle.italic = true;
638 lay.fontStyle.fontsize = fsizeText;
639 lay.fontStyle.color = colorText.asUint;
640 lay.lineStyle.leftpad = fsizeText*3;
641 lay.lineStyle.rightpad = 0;
642 lay.lineStyle.paraIndent = 0;
643 lay.lineStyle.setLeft;
644 //fixFont();
647 void setCodeStyle () {
648 lay.fontStyle.resetAttrs;
649 lay.fontStyle.monospace = true;
650 lay.fontStyle.italic = false;
651 lay.fontStyle.fontsize = fsizeText;
652 lay.fontStyle.color = colorText.asUint;
653 lay.lineStyle.leftpad = fsizeText*3;
654 lay.lineStyle.rightpad = fsizeText*3;
655 lay.lineStyle.paraIndent = 0;
656 lay.lineStyle.setLeft;
657 //lay.fontStyle.fontface = lay.fontFaceId("mono");
662 // ////////////////////////////////////////////////////////////////////////// //
663 private __gshared LayFontStash laf; // layouter font stash
666 // ////////////////////////////////////////////////////////////////////////// //
667 private void loadFmtFonts (/*NVGContext nvg*/) {
668 import std.functional : toDelegate;
669 if (laf is null) {
670 //if (nvg !is null) { import core.stdc.stdio : printf; printf("reusing NVG context...\n"); }
671 laf = new LayFontStash(/*nvg*/);
672 laf.fixFontDG = toDelegate(&FBFormatter.fixFont);
674 laf.addFont("text", textFontName);
675 laf.addFont("texti", textiFontName);
676 laf.addFont("textb", textbFontName);
677 laf.addFont("textz", textzFontName);
679 laf.addFont("mono", monoFontName);
680 laf.addFont("monoi", monoiFontName);
681 laf.addFont("monob", monobFontName);
682 laf.addFont("monoz", monozFontName);
687 // ////////////////////////////////////////////////////////////////////////// //
688 // messages
689 struct ReformatWork {
690 shared(BookText) booktext;
691 //shared(NVGContext) nvg; // can be null
692 string bookFileName;
693 int w, h;
696 struct ReformatWorkComplete {
697 int w, h;
698 shared(BookText) booktext;
699 shared(LayTextC) laytext;
700 shared(BookMetadata) meta;
703 struct QuitWork {
707 // ////////////////////////////////////////////////////////////////////////// //
708 void reformatThreadFn (Tid ownerTid) {
709 bool doQuit = false;
710 BookText book;
711 string newFileName;
712 int newW = -1, newH = -1;
713 //NVGContext lastnvg = null;
714 while (!doQuit) {
715 book = null;
716 newFileName = null;
717 newW = newH = -1;
718 receive(
719 (ReformatWork w) {
720 //{ conwriteln("reformat request received..."); }
721 book = cast(BookText)w.booktext;
722 newFileName = w.bookFileName;
723 newW = w.w;
724 newH = w.h;
725 if (newW < 1) newW = 1;
726 if (newH < 1) newH = 1;
727 //lastnvg = cast(NVGContext)w.nvg;
729 (QuitWork w) {
730 doQuit = true;
733 if (!doQuit && newW > 0 && newH > 0) {
734 try {
735 if (book is null) {
736 //conwriteln("loading new book: '", newFileName, "'");
737 book = loadBook(newFileName);
740 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
741 if (maxWidth < 64) maxWidth = 64;
743 loadFmtFonts();
745 // layout text
746 //conwriteln("layouting...");
747 auto fmtr = new FBFormatter();
749 auto stt = MonoTime.currTime;
751 auto lay = new LayText(laf, maxWidth);
752 lay.fontStyle.color = colorText.asUint;
753 auto meta = book.formatBook(lay);
755 fmtr.formatBook(book, maxWidth);
756 auto ett = MonoTime.currTime-stt;
758 auto meta = fmtr.meta;
759 auto lay = fmtr.lay;
761 conwriteln("layouted in ", ett.total!"msecs", " milliseconds");
762 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay, cast(shared)meta);
763 send(ownerTid, res);
764 } catch (Throwable e) {
765 // here, we are dead and fucked (the exact order doesn't matter)
766 import core.stdc.stdlib : abort;
767 import core.stdc.stdio : fprintf, stderr;
768 import core.memory : GC;
769 import core.thread : thread_suspendAll;
770 GC.disable(); // yeah
771 thread_suspendAll(); // stop right here, you criminal scum!
772 auto s = e.toString();
773 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
774 abort(); // die, you bitch!
778 send(ownerTid, QuitWork());