cosmetix
[xreader.git] / xreaderfmt.d
blob9188024c18a778a07545b6a226a130c64277211c
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.nanovg;
30 import iv.nanovg.oui.blendish;
31 import iv.nanovg.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 int 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 >= 0) {
101 if (++nfo.framesNotUsed > ImageInfo.MaxIdleFrames) {
102 //conwriteln("freeing image with id ", nfo.iid);
103 vg.deleteImage(nfo.iid);
104 nfo.iid = -1;
105 nfo.img = null;
113 private int registerImage (NVGContext vg, TrueColorImage img) {
114 if (vg is null || img is null) return -1;
115 uint freeIdx = uint.max;
116 foreach (immutable idx, ref ImageInfo nfo; nvgImageIds) {
117 if (nfo.img is img) {
118 assert(nfo.iid >= 0);
119 nfo.framesNotUsed = 0;
120 return nfo.iid;
122 if (freeIdx != uint.max && nfo.iid < 0) freeIdx = cast(uint)idx;
124 int iid = vg.createImageFromMemoryImage(img); //vg.createImageRGBA(img.width, img.height, img.imageData.bytes[], NVGImageFlags.None);
125 if (iid < 0) return -1;
126 nvgImageIds ~= ImageInfo(iid, img, 0);
127 return iid;
131 class BookImage : LayObject {
132 BookText book;
133 uint imageidx;
134 TrueColorImage img;
135 this (BookText abook, uint aidx) { book = abook; imageidx = aidx; img = book.images[aidx].img; }
136 override int width () { return img.width; }
137 override int spacewidth () { return 0; }
138 override int height () { return img.height; }
139 override int ascent () { return img.height; }
140 override int descent () { return 0; }
141 override bool canbreak () { return true; }
142 override bool spaced () { return false; }
143 override void draw (NVGContext vg, float x, float y) {
144 y -= img.height;
145 vg.save();
146 scope(exit) vg.restore();
147 vg.beginPath();
148 int iid = vg.registerImage(img);
149 if (iid < 0) return;
150 vg.fillPaint(vg.imagePattern(x, y, img.width, img.height, 0, iid));
151 //vg.globalAlpha(0.5);
152 vg.rect(x, y, img.width, img.height);
153 vg.fill();
158 // ////////////////////////////////////////////////////////////////////////// //
159 class BookMetadata {
160 static struct Anc {
161 dstring name;
162 uint wordidx; // first word
164 Anc[] sections;
165 Anc[uint] hrefs; // `name` is dest
166 Anc[] ids;
170 // ////////////////////////////////////////////////////////////////////////// //
171 final class FBFormatter {
172 private:
173 BookText book;
174 bool hasSections;
176 public:
177 LayTextC lay;
178 BookMetadata meta;
180 public:
181 this () {}
183 void formatBook (BookText abook, int maxWidth) {
184 assert(abook !is null);
186 auto oldFixFontDG = layFixFontDG;
187 scope(exit) layFixFontDG = oldFixFontDG;
189 layFixFontDG = null;
191 lay = new LayTextC(laf, maxWidth);
192 lay.fontStyle.color = colorText.asUint;
194 layFixFontDG = &fixFont;
196 meta = new BookMetadata();
198 book = abook;
199 scope(exit) book = null;
200 hasSections = false;
202 putMain();
204 lay.finalize();
207 private:
208 void dumpTree (Tag ct) {
209 while (ct !is null) {
210 conwriteln("tag: ", ct.name);
211 ct = ct.parent;
215 void badTag (Tag tag, string msg=null) {
216 import std.string : format, indexOf;
217 assert(tag !is null);
218 dumpTree(tag);
219 if (msg.length == 0) msg = "invalid tag: '%s'";
220 if (msg.indexOf("%s") >= 0) {
221 throw new Exception(msg.format(tag.name));
222 } else {
223 throw new Exception(msg);
227 void saveTagId (Tag tag) {
228 if (tag.id.length) {
229 import std.conv : to;
230 meta.ids ~= BookMetadata.Anc(tag.id.to!dstring, lay.nextWordIndex);
234 void registerSection (Tag tag) {
235 import std.conv : to;
236 string text = tag.textContent.xstrip;
237 //lay.sections ~= lay.curWordIndex;
238 string name;
239 while (text.length) {
240 char ch = text.ptr[0];
241 text = text[1..$];
242 if (ch <= ' ' || ch == 127) {
243 if (name.length == 0) continue;
244 if (ch != '\n') ch = ' ';
245 if (ch == '\n') {
246 if (name[$-1] > ' ') name ~= "\n...";
247 text = text.xstrip;
248 } else {
249 if (name[$-1] > ' ') name ~= ' ';
251 } else {
252 name ~= ch;
255 name = xstrip(name);
256 if (name.length == 0) name = "* * *";
257 meta.sections ~= BookMetadata.Anc(name.to!dstring, lay.nextWordIndex);
260 void putImage (Tag tag) {
261 if (tag is null || tag.href.length < 2 || tag.href[0] != '#') return;
262 string iid = tag.href[1..$];
263 //conwriteln("searching for image with href '", iid, "' (", book.images.length, ")");
264 foreach (immutable idx, ref BookText.Image img; book.images) {
265 if (iid == img.id) {
266 //conwriteln("image '", img.id, "' found");
267 lay.putObject(new BookImage(book, cast(uint)idx));
268 return;
273 void putOneTag(bool allowBad=false, bool allowText=true) (Tag tag) {
274 if (tag is null) return;
275 if (tag.name.length == 0) {
276 static if (allowText) {
277 saveTagId(tag);
278 lay.put(tag.text);
280 return;
282 switch (tag.name) {
283 case "p":
284 putTagContents(tag);
285 lay.endPara();
286 break;
288 case "strong":
289 lay.pushStyles();
290 scope(exit) lay.popStyles;
291 lay.fontStyle.bold = true;
292 putTagContents(tag);
293 break;
294 case "emphasis":
295 lay.pushStyles();
296 scope(exit) lay.popStyles;
297 lay.fontStyle.italic = true;
298 //fixFont();
299 putTagContents(tag);
300 break;
301 case "strikethrough":
302 lay.pushStyles();
303 scope(exit) lay.popStyles;
304 lay.fontStyle.strike = true;
305 //fixFont();
306 putTagContents(tag);
307 break;
309 case "empty-line":
310 lay.endLine();
311 break;
312 case "br":
313 lay.endPara();
314 break;
316 case "image":
317 lay.pushStyles();
318 scope(exit) lay.popStyles;
319 lay.endLine;
320 lay.lineStyle.setCenter;
321 putImage(tag);
322 lay.endLine;
323 return;
325 case "style":
326 //???
327 return;
329 case "a":
330 if (tag.href.length) {
331 import std.conv : to;
332 //conwriteln("href found: '", ct.href, "' (", lay.nextWordIndex, ")");
333 meta.hrefs[lay.nextWordIndex] = BookMetadata.Anc(tag.href.to!dstring, lay.nextWordIndex);
334 lay.pushStyles();
335 auto c = lay.fontStyle.color;
336 scope(exit) { lay.popStyles; lay.fontStyle.color = c; }
337 lay.fontStyle.href = true;
338 lay.fontStyle.underline = true;
339 lay.fontStyle.color = colorTextHref.asUint;
340 //fixFont();
341 putTagContents(tag);
342 } else {
343 putTagContents(tag);
345 break;
347 case "sub": // some idiotic files has this
348 case "sup": // some idiotic files has this
349 putTagContents(tag);
350 break;
352 case "code":
353 lay.pushStyles();
354 scope(exit) lay.popStyles;
355 setCodeStyle();
356 putTagContents(tag);
357 lay.endPara;
358 break;
360 case "poem":
361 lay.pushStyles();
362 scope(exit) lay.popStyles;
363 putPoem(tag);
364 //lay.endPara;
365 break;
367 case "stanza":
368 putTagContents(tag);
369 lay.endPara;
370 break;
372 case "v": // for stanzas
373 putTagContents(tag);
374 lay.endPara;
375 break;
377 case "date": // for poem
378 lay.endPara;
379 lay.pushStyles();
380 scope(exit) lay.popStyles;
381 lay.fontStyle.bold = true;
382 lay.fontStyle.italic = true;
383 lay.lineStyle.setRight;
384 //fixFont();
385 putTagContents(tag);
386 lay.endPara;
387 break;
389 case "cite":
390 putCite(tag);
391 return;
393 case "site":
394 case "annotation":
395 lay.pushStyles();
396 scope(exit) lay.popStyles;
397 lay.fontStyle.bold = true;
398 lay.lineStyle.setCenter;
399 //fixFont();
400 putTagContents(tag);
401 lay.endPara;
402 break;
404 case "text-author":
405 break;
407 case "title":
408 lay.pushStyles();
409 scope(exit) lay.popStyles;
410 setTitleStyle();
411 registerSection(tag);
412 putTagContents(tag);
413 if (tag.text.length == 0) lay.endPara();
414 break;
416 case "subtitle":
417 lay.pushStyles();
418 scope(exit) lay.popStyles;
419 setSubtitleStyle();
420 putTagContents(tag);
421 if (tag.text.length == 0) lay.endPara();
422 break;
424 case "epigraph":
425 lay.pushStyles();
426 scope(exit) lay.popStyles;
427 //auto olpad = lay.lineStyle.leftpad;
428 auto orpad = lay.lineStyle.rightpad;
429 setEpigraphStyle();
430 //lay.lineStyle.leftpad = lay.lineStyle.leftpad+olpad;
431 lay.lineStyle.rightpad = lay.lineStyle.rightpad+orpad;
432 putEpigraph(tag);
433 break;
435 case "section":
436 putTagContents(tag);
437 lay.endPara;
438 break;
440 case "table":
441 lay.pushStyles();
442 scope(exit) lay.popStyles;
443 lay.fontStyle.fontsize += 8;
444 lay.lineStyle.setCenter;
445 //fixFont();
446 lay.put("TABLE SKIPPED");
447 lay.endPara();
448 break;
450 default:
451 static if (allowBad) {
452 break;
453 } else {
454 badTag(tag);
459 bool onlyImagePara (Tag ct) {
460 if (ct is null) return false;
461 int count;
462 foreach (Tag tag; ct.children) {
463 if (tag.name.length == 0) {
464 // text
465 if (tag.text.xstrip.length != 0) return false;
466 continue;
468 if (count != 0 || tag.name != "image") return false;
469 ++count;
471 return (count == 1);
474 void putTagContents/*(bool xdump=false)*/ (Tag ct) {
475 if (ct is null) return;
476 saveTagId(ct);
477 if (onlyImagePara(ct)) {
478 lay.pushStyles();
479 scope(exit) lay.popStyles;
480 lay.endLine;
481 lay.lineStyle.setCenter;
482 foreach (Tag tag; ct.children) putOneTag(tag);
483 lay.endLine;
484 } else {
485 //if (ct.name == "subtitle") conwriteln("text: [", ct.children[0].text, "]");
486 foreach (Tag tag; ct.children) {
487 //static if (xdump) conwriteln("text: [", tag.text, "]");
488 putOneTag(tag);
491 //lay.endPara();
494 void putParas (Tag ct) {
495 if (ct is null) return;
496 putTagContents(ct);
497 lay.endPara();
500 void putAuthor (Tag ct) {
501 saveTagId(ct);
502 foreach (Tag tag; ct.children) {
503 if (tag.name.length == 0) continue;
504 if (tag.name == "text-author") {
505 putTagContents(tag);
506 lay.endPara();
511 void putCite (Tag ct) {
512 lay.pushStyles();
513 scope(exit) lay.popStyles;
514 setCiteStyle();
515 putParas(ct);
516 putAuthor(ct);
519 void putEpigraph (Tag ct) {
520 putParas(ct);
521 putAuthor(ct);
524 void putPoem (Tag ct) {
525 saveTagId(ct);
526 lay.pushStyles();
527 scope(exit) lay.popStyles;
528 setPoemStyle();
529 // put epigraph (not yet)
530 // put title and subtitle
531 foreach (Tag tag; ct.children) {
532 if (tag.name == "title") {
533 lay.pushStyles();
534 scope(exit) lay.popStyles;
535 if (!hasSections) registerSection(tag);
536 putParas(tag);
537 lay.endPara(); // space
538 } else if (tag.name == "subtitle") {
539 lay.pushStyles();
540 scope(exit) lay.popStyles;
541 putParas(tag);
542 //lay.endPara(); // space
545 lay.endPara;
546 foreach (Tag tag; ct.children) {
547 putOneTag!(false, false)(tag); // skip text without tags
549 putAuthor(ct);
552 void putMain () {
553 setNormalStyle();
554 foreach (Tag tag; book.content.children) {
555 putOneTag(tag);
559 private:
560 void fixFont (ref LayFontStyle st) nothrow @safe @nogc {
561 if (st.monospace) {
562 if (st.italic) {
563 if (st.bold) st.fontface = lay.fontFaceId("monoz");
564 else st.fontface = lay.fontFaceId("monoi");
565 } else if (st.bold) {
566 if (st.italic) st.fontface = lay.fontFaceId("monoz");
567 else st.fontface = lay.fontFaceId("monob");
568 } else {
569 st.fontface = lay.fontFaceId("mono");
571 } else {
572 if (st.italic) {
573 if (st.bold) st.fontface = lay.fontFaceId("textz");
574 else st.fontface = lay.fontFaceId("texti");
575 } else if (st.bold) {
576 if (st.italic) st.fontface = lay.fontFaceId("textz");
577 else st.fontface = lay.fontFaceId("textb");
578 } else {
579 st.fontface = lay.fontFaceId("text");
584 void setNormalStyle () {
585 lay.fontStyle.resetAttrs;
586 lay.fontStyle.fontsize = fsizeText;
587 lay.fontStyle.color = colorText.asUint;
588 lay.lineStyle.leftpad = 0;
589 lay.lineStyle.rightpad = 0;
590 lay.lineStyle.paraIndent = 3;
591 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
592 //fixFont();
595 void setTitleStyle () {
596 lay.fontStyle.resetAttrs;
597 lay.fontStyle.bold = true;
598 lay.fontStyle.fontsize = fsizeText+4;
599 lay.fontStyle.color = colorText.asUint;
600 lay.lineStyle.leftpad = 0;
601 lay.lineStyle.rightpad = 0;
602 lay.lineStyle.paraIndent = 0;
603 lay.lineStyle.setCenter;
604 //fixFont();
607 void setSubtitleStyle () {
608 lay.fontStyle.resetAttrs;
609 lay.fontStyle.bold = true;
610 lay.fontStyle.fontsize = fsizeText+2;
611 lay.fontStyle.color = colorText.asUint;
612 lay.lineStyle.leftpad = 0;
613 lay.lineStyle.rightpad = 0;
614 lay.lineStyle.paraIndent = 0;
615 lay.lineStyle.setCenter;
616 //fixFont();
619 void setCiteStyle () {
620 lay.fontStyle.resetAttrs;
621 lay.fontStyle.italic = true;
622 lay.fontStyle.fontsize = fsizeText;
623 lay.fontStyle.color = colorText.asUint;
624 lay.lineStyle.leftpad = fsizeText*3;
625 lay.lineStyle.rightpad = fsizeText*3;
626 lay.lineStyle.paraIndent = 3;
627 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
628 //fixFont();
631 void setEpigraphStyle () {
632 lay.fontStyle.resetAttrs;
633 lay.fontStyle.italic = true;
634 lay.fontStyle.fontsize = fsizeText;
635 lay.fontStyle.color = colorText.asUint;
636 lay.lineStyle.leftpad = lay.width/3;
637 lay.lineStyle.rightpad = 0;
638 lay.lineStyle.paraIndent = 0;
639 lay.lineStyle.setRight;
640 //fixFont();
643 void setPoemStyle () {
644 lay.fontStyle.resetAttrs;
645 lay.fontStyle.italic = true;
646 lay.fontStyle.fontsize = fsizeText;
647 lay.fontStyle.color = colorText.asUint;
648 lay.lineStyle.leftpad = fsizeText*3;
649 lay.lineStyle.rightpad = 0;
650 lay.lineStyle.paraIndent = 0;
651 lay.lineStyle.setLeft;
652 //fixFont();
655 void setCodeStyle () {
656 lay.fontStyle.resetAttrs;
657 lay.fontStyle.monospace = true;
658 lay.fontStyle.italic = false;
659 lay.fontStyle.fontsize = fsizeText;
660 lay.fontStyle.color = colorText.asUint;
661 lay.lineStyle.leftpad = fsizeText*3;
662 lay.lineStyle.rightpad = fsizeText*3;
663 lay.lineStyle.paraIndent = 0;
664 lay.lineStyle.setLeft;
665 //lay.fontStyle.fontface = lay.fontFaceId("mono");
670 // ////////////////////////////////////////////////////////////////////////// //
671 private __gshared LayFontStash laf; // layouter font stash
674 // ////////////////////////////////////////////////////////////////////////// //
675 private void loadFmtFonts (/*NVGContext nvg*/) {
676 if (laf is null) {
677 //if (nvg !is null) { import core.stdc.stdio : printf; printf("reusing NVG context...\n"); }
678 laf = new LayFontStash(/*nvg*/);
680 laf.addFont("text", textFontName);
681 laf.addFont("texti", textiFontName);
682 laf.addFont("textb", textbFontName);
683 laf.addFont("textz", textzFontName);
685 laf.addFont("mono", monoFontName);
686 laf.addFont("monoi", monoiFontName);
687 laf.addFont("monob", monobFontName);
688 laf.addFont("monoz", monozFontName);
693 // ////////////////////////////////////////////////////////////////////////// //
694 // messages
695 struct ReformatWork {
696 shared(BookText) booktext;
697 //shared(NVGContext) nvg; // can be null
698 string bookFileName;
699 int w, h;
702 struct ReformatWorkComplete {
703 int w, h;
704 shared(BookText) booktext;
705 shared(LayTextC) laytext;
706 shared(BookMetadata) meta;
709 struct QuitWork {
713 // ////////////////////////////////////////////////////////////////////////// //
714 void reformatThreadFn (Tid ownerTid) {
715 bool doQuit = false;
716 BookText book;
717 string newFileName;
718 int newW = -1, newH = -1;
719 //NVGContext lastnvg = null;
720 while (!doQuit) {
721 book = null;
722 newFileName = null;
723 newW = newH = -1;
724 receive(
725 (ReformatWork w) {
726 //{ conwriteln("reformat request received..."); }
727 book = cast(BookText)w.booktext;
728 newFileName = w.bookFileName;
729 newW = w.w;
730 newH = w.h;
731 if (newW < 1) newW = 1;
732 if (newH < 1) newH = 1;
733 //lastnvg = cast(NVGContext)w.nvg;
735 (QuitWork w) {
736 doQuit = true;
739 if (!doQuit && newW > 0 && newH > 0) {
740 try {
741 if (book is null) {
742 //conwriteln("loading new book: '", newFileName, "'");
743 book = loadBook(newFileName);
746 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
747 if (maxWidth < 64) maxWidth = 64;
749 loadFmtFonts();
751 // layout text
752 //conwriteln("layouting...");
753 auto fmtr = new FBFormatter();
755 auto stt = MonoTime.currTime;
757 auto lay = new LayText(laf, maxWidth);
758 lay.fontStyle.color = colorText.asUint;
759 auto meta = book.formatBook(lay);
761 fmtr.formatBook(book, maxWidth);
762 auto ett = MonoTime.currTime-stt;
764 auto meta = fmtr.meta;
765 auto lay = fmtr.lay;
767 conwriteln("layouted in ", ett.total!"msecs", " milliseconds");
768 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay, cast(shared)meta);
769 send(ownerTid, res);
770 } catch (Throwable e) {
771 // here, we are dead and fucked (the exact order doesn't matter)
772 import core.stdc.stdlib : abort;
773 import core.stdc.stdio : fprintf, stderr;
774 import core.memory : GC;
775 import core.thread : thread_suspendAll;
776 GC.disable(); // yeah
777 thread_suspendAll(); // stop right here, you criminal scum!
778 auto s = e.toString();
779 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
780 abort(); // die, you bitch!
784 send(ownerTid, QuitWork());