adapted to new nanovg API
[xreader.git] / xreaderfmt.d
blob720341da5273c60e9c02161c88bdf9f50740fbcd
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.color;
27 import arsd.png;
28 import arsd.jpeg;
30 import iv.nanovg;
31 import iv.nanovg.oui.blendish;
32 import iv.vfs;
33 import iv.vfs.io;
35 import booktext;
37 import xreadercfg;
38 import xlayouter;
41 // ////////////////////////////////////////////////////////////////////////// //
42 BookText loadBook (string fname) {
43 __gshared bool eliteShipsLoaded = false;
45 import std.path;
46 //import core.memory : GC;
47 fname = fname.expandTilde.absolutePath;
48 //writeln("loading '", fname, "'...");
49 //GC.disable();
50 auto stt = MonoTime.currTime;
51 auto book = new BookText(fname);
52 //GC.enable();
53 //GC.collect();
54 { import std.stdio; writeln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds"); }
56 string[] lines;
57 string[] comments;
58 try {
59 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
60 if (line != fname) {
61 lines ~= line;
62 } else {
63 while (lines.length && lines[$-1].isComment) {
64 comments ~= lines[$-1];
65 lines = lines[0..$-1];
69 } catch (Exception) {}
70 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
71 auto fo = VFile(buildPath(RcDir, ".lastfile"), "w");
72 foreach (string s; lines) fo.writeln(s);
73 foreach_reverse (string s; comments) fo.writeln(s);
74 fo.writeln(fname);
76 stateFileName = buildPath(RcDir, fname.baseName~".rc");
78 if (!eliteShipsLoaded) {
79 loadEliteShips();
80 eliteShipsLoaded = true;
83 return book;
87 // ////////////////////////////////////////////////////////////////////////// //
88 private __gshared int[] nvgImageIds;
90 void releaseImages (NVGContext vg) {
91 if (nvgImageIds.length) {
92 foreach (int iid; nvgImageIds) vg.deleteImage(iid);
93 nvgImageIds.length = 0;
94 nvgImageIds.assumeSafeAppend;
99 class BookImage : LayObject {
100 BookText book;
101 uint imageidx;
102 TrueColorImage img;
103 this (BookText abook, uint aidx) { book = abook; imageidx = aidx; img = book.images[aidx].img; }
104 override int width () { return img.width; }
105 override int spacewidth () { return 0; }
106 override int height () { return img.height; }
107 override int ascent () { return img.height; }
108 override int descent () { return 0; }
109 override bool canbreak () { return true; }
110 override bool spaced () { return false; }
111 override void draw (NVGContext vg, float x, float y) {
112 y -= img.height;
113 vg.save();
114 scope(exit) vg.restore();
115 vg.beginPath();
116 int iid = vg.createImageFromMemoryImage(img); //vg.createImageRGBA(img.width, img.height, img.imageData.bytes[], NVGImageFlags.None);
117 if (iid < 0) { writeln("WTF?!"); iid = -666; return; }
118 nvgImageIds ~= iid;
119 vg.fillPaint(vg.imagePattern(x, y, img.width, img.height, 0, iid));
120 //vg.globalAlpha(0.5);
121 vg.rect(x, y, img.width, img.height);
122 vg.fill();
127 // ////////////////////////////////////////////////////////////////////////// //
128 class BookMetadata {
129 static struct Anc {
130 dstring name;
131 uint wordidx; // first word
133 Anc[] sections;
134 Anc[] hrefs; // `name` is dest
135 Anc[] ids;
139 // ////////////////////////////////////////////////////////////////////////// //
140 public BookMetadata formatBook (BookText book, LayText lay) {
141 assert(book !is null);
143 BookMetadata meta = new BookMetadata();
145 static void fixFont (LayText lay) {
146 if (lay.fontStyle.italic) {
147 if (lay.fontStyle.bold) lay.fontStyle.fontface = lay.fontFaceId("textz");
148 else lay.fontStyle.fontface = lay.fontFaceId("texti");
149 } else if (lay.fontStyle.bold) {
150 if (lay.fontStyle.italic) lay.fontStyle.fontface = lay.fontFaceId("textz");
151 else lay.fontStyle.fontface = lay.fontFaceId("textb");
152 } else {
153 lay.fontStyle.fontface = lay.fontFaceId("text");
157 static void setNormalStyle (LayText lay) {
158 lay.fontStyle.resetAttrs;
159 lay.fontStyle.fontsize = fsizeText;
160 lay.fontStyle.color = colorText.asUint;
161 lay.lineStyle.leftpad = 0;
162 lay.lineStyle.rightpad = 0;
163 lay.lineStyle.paraIndent = 3;
164 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
165 fixFont(lay);
168 static void setTitleStyle (LayText lay) {
169 lay.fontStyle.resetAttrs;
170 lay.fontStyle.bold = true;
171 lay.fontStyle.fontsize = fsizeText+4;
172 lay.fontStyle.color = colorText.asUint;
173 lay.lineStyle.leftpad = 0;
174 lay.lineStyle.rightpad = 0;
175 lay.lineStyle.paraIndent = 0;
176 lay.lineStyle.setCenter;
177 fixFont(lay);
180 static void setSubtitleStyle (LayText lay) {
181 lay.fontStyle.resetAttrs;
182 lay.fontStyle.bold = true;
183 lay.fontStyle.fontsize = fsizeText+2;
184 lay.fontStyle.color = colorText.asUint;
185 lay.lineStyle.leftpad = 0;
186 lay.lineStyle.rightpad = 0;
187 lay.lineStyle.paraIndent = 0;
188 lay.lineStyle.setCenter;
189 fixFont(lay);
192 static void setCiteStyle (LayText lay) {
193 lay.fontStyle.resetAttrs;
194 lay.fontStyle.italic = true;
195 lay.fontStyle.fontsize = fsizeText;
196 lay.fontStyle.color = colorText.asUint;
197 lay.lineStyle.leftpad = fsizeText*3;
198 lay.lineStyle.rightpad = fsizeText*3;
199 lay.lineStyle.paraIndent = 3;
200 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
201 fixFont(lay);
204 static void setEpigraphStyle (LayText lay) {
205 lay.fontStyle.resetAttrs;
206 lay.fontStyle.italic = true;
207 lay.fontStyle.fontsize = fsizeText;
208 lay.fontStyle.color = colorText.asUint;
209 lay.lineStyle.leftpad = lay.width/2;
210 lay.lineStyle.rightpad = 0;
211 lay.lineStyle.paraIndent = 0;
212 lay.lineStyle.setRight;
213 fixFont(lay);
216 static void setPoemStyle (LayText lay) {
217 lay.fontStyle.resetAttrs;
218 lay.fontStyle.italic = true;
219 lay.fontStyle.fontsize = fsizeText;
220 lay.fontStyle.color = colorText.asUint;
221 lay.lineStyle.leftpad = fsizeText*3;
222 lay.lineStyle.rightpad = 0;
223 lay.lineStyle.paraIndent = 0;
224 lay.lineStyle.setLeft;
225 fixFont(lay);
228 void dumpTree (Tag ct) {
229 while (ct !is null) {
230 { import std.stdio; writeln("tag: ", ct.name); }
231 ct = ct.parent;
235 void badTag (Tag tag, string msg=null) {
236 import std.string : format, indexOf;
237 assert(tag !is null);
238 dumpTree(tag);
239 if (msg.length == 0) msg = "invalid tag: '%s'";
240 if (msg.indexOf("%s") >= 0) {
241 throw new Exception(msg.format(tag.name));
242 } else {
243 throw new Exception(msg);
247 void saveTagId (Tag tag) {
248 if (tag.id.length) {
249 import std.conv : to;
250 meta.ids ~= BookMetadata.Anc(tag.id.to!dstring, lay.nextWordIndex);
254 void putImage (Tag tag) {
255 if (tag is null || tag.href.length < 2 || tag.href[0] != '#') return;
256 string iid = tag.href[1..$];
257 //writeln("searching for image with href '", iid, "' (", book.images.length, ")");
258 foreach (immutable idx, ref BookText.Image img; book.images) {
259 if (iid == img.id) {
260 //writeln("image '", img.id, "' found");
261 lay.putObject(new BookImage(book, cast(uint)idx));
262 return;
267 void putParaContentInternal (Tag ct, int boldc=0, int italicc=0, int underc=0) {
268 if (ct is null) return;
269 auto c = lay.fontStyle.color;
270 scope(exit) lay.fontStyle.color = c;
271 saveTagId(ct);
272 switch (ct.name) {
273 case "strong": ++boldc; lay.fontStyle.bold = true; break;
274 case "emphasis": ++italicc; lay.fontStyle.italic = true; break;
275 case "image": putImage(ct); return;
276 case "style": return;
277 case "a":
278 if (ct.href.length) {
279 import std.conv : to;
280 meta.hrefs ~= BookMetadata.Anc(ct.href.to!dstring, lay.nextWordIndex);
281 ++underc;
282 lay.fontStyle.underline = true;
283 lay.fontStyle.color = colorTextHref.asUint;
285 break;
286 case "": lay.put(ct.text); return;
287 default: badTag(ct);
289 fixFont(lay);
290 foreach (Tag tag; ct.children) putParaContentInternal(tag, boldc, italicc, underc);
291 switch (ct.name) {
292 case "strong": if (--boldc == 0) lay.fontStyle.bold = false; break;
293 case "emphasis": if (--italicc == 0) lay.fontStyle.italic = false; break;
294 case "a": if (--underc == 0) lay.fontStyle.underline = false; break;
295 default:
297 fixFont(lay);
300 void putTagContents (Tag ct) {
301 if (ct is null) return;
302 saveTagId(ct);
303 foreach (Tag tag; ct.children) putParaContentInternal(tag);
304 lay.endPara();
307 void putParas (Tag ct) {
308 saveTagId(ct);
309 foreach (Tag tag; ct.children) {
310 if (tag.name.length == 0) continue;
311 if (tag.name == "p") putTagContents(tag);
312 else if (tag.name == "empty-line") lay.endLine();
313 else if (tag.name == "text-author") {}
314 else badTag(tag);
318 void putAuthor (Tag ct) {
319 saveTagId(ct);
320 foreach (Tag tag; ct.children) {
321 if (tag.name.length == 0) continue;
322 if (tag.name == "text-author") {
323 putTagContents(tag);
324 lay.endPara();
329 void putCite (Tag ct) {
330 lay.pushStyles();
331 scope(exit) lay.popStyles;
332 setCiteStyle(lay);
333 putParas(ct);
334 putAuthor(ct);
337 void putEpigraph (Tag ct) {
338 lay.pushStyles();
339 scope(exit) lay.popStyles;
340 setEpigraphStyle(lay);
341 putParas(ct);
342 putAuthor(ct);
345 void putStanza (Tag ct) {
346 saveTagId(ct);
347 foreach (Tag tag; ct.children) {
348 if (tag.name.length == 0) continue;
349 if (tag.name == "text-author") continue;
350 if (tag.name == "title") badTag(tag, "titles in stanzas are not supported yet");
351 if (tag.name == "subtitle") badTag(tag, "subtitles in stanzas are not supported yet");
352 if (tag.name == "epigraph") badTag(tag, "epigraphs in poems are not supported yet");
353 if (tag.name == "date") continue;
354 if (tag.name == "v") { putTagContents(tag); continue; }
355 badTag(tag);
359 void putPoem (Tag ct) {
360 saveTagId(ct);
361 lay.pushStyles();
362 scope(exit) lay.popStyles;
363 setPoemStyle(lay);
364 // put epigraph (not yet)
365 // put title and subtitle
366 foreach (Tag tag; ct.children) {
367 if (tag.name == "title") {
368 lay.pushStyles();
369 scope(exit) lay.popStyles;
370 putParas(tag);
371 lay.endPara(); // space
372 } else if (tag.name == "subtitle") {
373 lay.pushStyles();
374 scope(exit) lay.popStyles;
375 putParas(tag);
376 lay.endPara(); // space
379 foreach (Tag tag; ct.children) {
380 if (tag.name.length == 0) continue;
381 if (tag.name == "text-author") continue;
382 if (tag.name == "title") continue;
383 if (tag.name == "subtitle") continue;
384 if (tag.name == "epigraph") badTag(tag, "epigraphs in poems are not supported yet");
385 if (tag.name == "date") continue;
386 if (tag.name == "stanza") { putStanza(tag); lay.put(LayText.EndParaCh); continue; }
387 badTag(tag);
389 putAuthor(ct);
392 void putSection (Tag sc) {
393 bool sectionRegistered = false;
395 void registerSection (Tag tag) {
396 import std.conv : to;
397 if (sectionRegistered) return;
398 sectionRegistered = true;
399 string text = tag.textContent.xstrip;
400 //lay.sections ~= lay.curWordIndex;
401 string name;
402 while (text.length) {
403 char ch = text.ptr[0];
404 text = text[1..$];
405 if (ch <= ' ' || ch == 127) {
406 if (name.length == 0) continue;
407 if (ch != '\n') ch = ' ';
408 if (ch == '\n') {
409 if (name[$-1] > ' ') name ~= "\n...";
410 text = text.xstrip;
411 } else {
412 if (name[$-1] > ' ') name ~= ' ';
414 } else {
415 name ~= ch;
418 name = xstrip(name);
419 if (name.length == 0) name = "* * *";
420 meta.sections ~= BookMetadata.Anc(name.to!dstring, lay.nextWordIndex);
423 saveTagId(sc);
424 foreach (Tag tag; sc.children) {
425 if (tag.name.length == 0) continue;
426 if (tag.name == "title") {
427 lay.pushStyles();
428 scope(exit) lay.popStyles;
429 setTitleStyle(lay);
430 registerSection(tag);
431 putParas(tag);
432 } else if (tag.name == "subtitle") {
433 lay.pushStyles();
434 scope(exit) lay.popStyles;
435 setSubtitleStyle(lay);
436 registerSection(tag);
437 putParas(tag);
438 } else if (tag.name == "epigraph") {
439 lay.pushStyles();
440 scope(exit) lay.popStyles;
441 setEpigraphStyle(lay);
442 putEpigraph(tag);
443 } else if (tag.name == "section") {
444 putSection(tag);
445 } else if (tag.name == "p") {
446 putTagContents(tag);
447 } else if (tag.name == "image") {
448 lay.pushStyles();
449 scope(exit) lay.popStyles;
450 lay.lineStyle.leftpad = 0;
451 lay.lineStyle.rightpad = 0;
452 lay.lineStyle.paraIndent = 0;
453 lay.lineStyle.setCenter;
454 putImage(tag);
455 lay.endPara();
456 } else if (tag.name == "empty-line") {
457 lay.endPara();
458 } else if (tag.name == "cite") {
459 putCite(tag);
460 } else if (tag.name == "table") {
461 lay.pushStyles();
462 scope(exit) lay.popStyles;
463 lay.fontStyle.fontsize += 8;
464 lay.lineStyle.setCenter;
465 lay.put("TABLE SKIPPED");
466 lay.endPara();
467 } else if (tag.name == "poem") {
468 putPoem(tag);
469 } else if (tag.name == "image") {
470 } else if (tag.name == "style") {
471 } else {
472 badTag(tag);
477 foreach (Tag tag; book.content.children) {
478 if (tag.name == "title") {
479 lay.pushStyles();
480 scope(exit) lay.popStyles;
481 setTitleStyle(lay);
482 lay.endPara();
483 putParas(tag);
484 lay.endPara();
485 } else if (tag.name == "subtitle") {
486 lay.pushStyles();
487 scope(exit) lay.popStyles;
488 setTitleStyle(lay);
489 putParas(tag);
490 lay.endPara();
491 } else if (tag.name == "epigraph") {
492 putEpigraph(tag);
493 } else if (tag.name == "section") {
494 lay.pushStyles();
495 scope(exit) lay.popStyles;
496 setNormalStyle(lay);
497 putSection(tag);
498 } else if (tag.name == "image") {
499 putImage(tag);
503 lay.finalize();
505 return meta;
509 // ////////////////////////////////////////////////////////////////////////// //
510 private __gshared LayFontStash laf; // layouter font stash
513 // ////////////////////////////////////////////////////////////////////////// //
514 private void loadFmtFonts () {
515 laf = new LayFontStash();
517 laf.addFont("text", textFontName);
518 laf.addFont("texti", textiFontName);
519 laf.addFont("textb", textbFontName);
520 laf.addFont("textz", textzFontName);
522 laf.addFont("mono", monoFontName);
523 laf.addFont("monoi", monoiFontName);
524 laf.addFont("monob", monobFontName);
525 laf.addFont("monoz", monozFontName);
529 // ////////////////////////////////////////////////////////////////////////// //
530 // messages
531 struct ReformatWork {
532 shared(BookText) booktext;
533 string bookFileName;
534 int w, h;
537 struct ReformatWorkComplete {
538 int w, h;
539 shared(BookText) booktext;
540 shared(LayText) laytext;
541 shared(BookMetadata) meta;
544 struct QuitWork {
548 // ////////////////////////////////////////////////////////////////////////// //
549 void reformatThreadFn (Tid ownerTid) {
550 bool doQuit = false;
551 loadFmtFonts();
552 BookText book;
553 string newFileName;
554 int newW = -1, newH = -1;
555 while (!doQuit) {
556 book = null;
557 newFileName = null;
558 newW = newH = -1;
559 receive(
560 (ReformatWork w) {
561 //{ import std.stdio; writeln("reformat request received..."); }
562 book = cast(BookText)w.booktext;
563 newFileName = w.bookFileName;
564 newW = w.w;
565 newH = w.h;
566 if (newW < 1) newW = 1;
567 if (newH < 1) newH = 1;
569 (QuitWork w) {
570 doQuit = true;
573 if (!doQuit && newW > 0 && newH > 0) {
574 try {
575 if (book is null) {
576 //writeln("loading new book: '", newFileName, "'");
577 book = loadBook(newFileName);
580 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
581 if (maxWidth < 64) maxWidth = 64;
583 // layout text
584 //writeln("layouting...");
585 auto stt = MonoTime.currTime;
586 auto lay = new LayText(laf, maxWidth);
587 lay.fontStyle.color = colorText.asUint;
589 auto meta = book.formatBook(lay);
591 auto ett = MonoTime.currTime-stt;
592 writeln("layouted in ", ett.total!"msecs", " milliseconds");
593 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay, cast(shared)meta);
594 send(ownerTid, res);
595 } catch (Throwable e) {
596 // here, we are dead and fucked (the exact order doesn't matter)
597 import core.stdc.stdlib : abort;
598 import core.stdc.stdio : fprintf, stderr;
599 import core.memory : GC;
600 import core.thread : thread_suspendAll;
601 GC.disable(); // yeah
602 thread_suspendAll(); // stop right here, you criminal scum!
603 auto s = e.toString();
604 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
605 abort(); // die, you bitch!
609 send(ownerTid, QuitWork());