epigraph width fix
[xreader.git] / xreader.d
blobce3b2d0de24edd497f203ee1cd9771afe5080ce2
1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>f
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 xreader;
19 import core.time;
21 import std.concurrency;
23 import arsd.simpledisplay;
24 import arsd.image;
26 import iv.nanovg;
27 import iv.nanovg.oui.blendish;
28 import iv.nanovg.perf;
30 import iv.strex;
32 import iv.vfs;
33 import iv.vfs.io;
35 import xmodel;
36 import xiniz;
38 import booktext;
40 import xreadercfg;
41 import xreaderui;
42 import xreaderifs;
43 import xreaderfmt;
44 import xlayouter;
46 import eworld;
47 import ewbillboard;
49 //version = debug_draw;
52 // ////////////////////////////////////////////////////////////////////////// //
53 void run (string bookFileName) {
54 int formatWorks = -1;
55 NVGContext vg = null;
56 PerfGraph fps;
57 bool fpsVisible = false;
58 BookText booktext;
59 string newBookFileName;
61 if (GWidth < MinWinWidth) GWidth = MinWinWidth;
62 if (GHeight < MinWinHeight) GHeight = MinWinHeight;
64 uint[] posstack;
66 bool doQuit = false;
68 booktext = loadBook(bookFileName);
70 //setOpenGLContextVersion(3, 2); // up to GLSL 150
71 setOpenGLContextVersion(2, 0); // it's enough
72 //openGLContextCompatible = false;
74 auto sdwindow = new SimpleWindow(GWidth, GHeight, booktext.title~" \xe2\x80\x94 "~booktext.authorFirst~" "~booktext.authorLast, OpenGlOptions.yes, Resizablity.allowResizing);
75 sdwindow.hideCursor(); // we will do our own
76 sdwindow.setMinSize(MinWinWidth, MinWinHeight);
78 auto stt = MonoTime.currTime;
79 auto prevt = MonoTime.currTime;
80 auto curt = prevt;
81 float dt = 0, secs = 0;
82 LayText laytext;
83 int textHeight = GHeight-8;
84 bool doSaveCheck = false;
85 MonoTime nextSaveTime;
87 MonoTime nextIfTime = MonoTime.currTime;
89 BookInfo[] recentFiles;
90 BookMetadata bookmeta;
92 int toMove = 0; // for smooth scroller
93 int topY = 0;
94 int arrowDir = 0;
96 int newYLine = -1; // index
97 float newYAlpha;
98 bool newYFade = false;
99 MonoTime nextFadeTime;
101 int curImg = -1, curImgWhite = -1;
102 int mouseX = -666, mouseY = -666;
103 bool mouseHigh = false;
104 bool mouseHidden = false;
105 auto lastMMove = MonoTime.currTime;
107 bool needRedrawFlag = true;
109 bool firstFormat = true;
111 bool inGalaxyMap = false;
112 PopupMenu currPopup;
113 void delegate (int item) onPopupSelect;
114 int shipModelIndex = 0;
115 bool popupNoShipKill = false;
117 void refresh () { needRedrawFlag = true; }
118 void refreshed () { needRedrawFlag = false; }
120 bool needRedraw () {
121 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
124 @property bool inMenu () { return (currPopup !is null); }
125 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; }
127 auto childTid = spawn(&reformatThreadFn, thisTid);
128 childTid.setMaxMailboxSize(128, OnCrowding.block);
129 thisTid.setMaxMailboxSize(128, OnCrowding.block);
131 void setShip (int idx) {
132 if (eliteShipFiles.length) {
133 import core.memory : GC;
134 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
135 if (idx < 0) idx = 0;
136 if (shipModelIndex == idx && shipModel !is null) return;
137 // remove old ship
138 shipModelIndex = idx;
139 if (shipModel !is null) {
140 shipModel.glUnload();
141 shipModel.freeData();
142 shipModel.destroy;
143 shipModel = null;
145 GC.collect();
146 // load new ship
147 try {
148 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
149 shipModel.glUpload();
150 shipModel.freeImages();
151 } catch (Exception e) {}
152 //shipModel = eliteShips[shipModelIndex];
153 GC.collect();
157 void ensureShipModel () {
158 if (eliteShipFiles.length && shipModel is null) {
159 import std.random : uniform;
160 setShip(uniform!"[)"(0, eliteShipFiles.length));
164 void freeShipModel () {
165 if (!showShip && !inMenu && shipModel !is null) {
166 import core.memory : GC;
167 shipModel.glUnload();
168 shipModel.freeData();
169 shipModel.destroy;
170 shipModel = null;
171 GC.collect();
175 void doSaveState (bool forced=false) {
176 if (!forced) {
177 if (formatWorks != 0) return;
178 if (!doSaveCheck) return;
179 auto ct = MonoTime.currTime;
180 if (ct < nextSaveTime) return;
181 } else {
182 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
184 try {
185 auto fo = VFile(stateFileName, "w");
186 if (laytext !is null && laytext.lineCount) {
187 auto lnum = laytext.findLineAtY(topY);
188 if (lnum >= 0) fo.writeln("wordindex=", laytext.line(lnum).wstart);
190 } catch (Exception) {}
191 doSaveCheck = false;
194 void stateChanged () {
195 if (!doSaveCheck) {
196 doSaveCheck = true;
197 nextSaveTime = MonoTime.currTime+10.seconds;
201 void hardScrollBy (int delta) {
202 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
203 int oldY = topY;
204 topY += delta;
205 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
206 if (topY < 0) topY = 0;
207 if (topY != oldY) {
208 stateChanged();
209 refresh();
213 void scrollBy (int delta) {
214 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
215 toMove += delta;
216 newYFade = true;
217 newYAlpha = 1;
218 if (delta < 0) {
219 // scrolling up, mark top line
220 newYLine = laytext.findLineAtY(topY);
221 } else {
222 // scrolling down, mark bottom line
223 newYLine = laytext.findLineAtY(topY+textHeight-2);
225 version(none) {
226 import std.stdio;
227 writeln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
229 if (newYLine < 0) newYFade = false;
232 void goHome () {
233 if (laytext is null) return;
234 if (topY != 0) {
235 topY = 0;
236 stateChanged();
237 refresh();
241 void goTo (uint widx) {
242 if (laytext is null) return;
243 auto lidx = laytext.findLineWithWord(widx);
244 if (lidx != -1) {
245 assert(lidx < laytext.lineCount);
246 toMove = 0;
247 if (topY != laytext.line(lidx).y) {
248 topY = laytext.line(lidx).y;
249 stateChanged();
250 refresh();
252 version(none) {
253 import std.stdio;
254 writeln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
255 auto lnum = laytext.findLineAtY(topY);
256 writeln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
261 void loadState () {
262 try {
263 int widx = -1;
264 xiniParse(VFile(stateFileName),
265 "wordindex", &widx,
267 if (widx >= 0) goTo(widx);
268 } catch (Exception) {}
271 void loadAndFormat (string filename) {
272 assert(formatWorks <= 0);
273 booktext = null;
274 laytext = null;
275 newYLine = -1;
276 //formatWorks = -1; //FIXME
277 firstFormat = true;
278 newYFade = false;
279 toMove = 0;
280 recentFiles = null;
281 arrowDir = 0;
282 //sdwindow.redrawOpenGlSceneNow();
283 //booktext = loadBook(newBookFileName);
284 //newBookFileName = null;
285 //reformat();
286 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
287 formatWorks = 1;
288 //writeln("*** loading new book: '", filename, "'");
289 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
290 refresh();
293 void reformat () {
294 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
295 childTid.send(ReformatWork(cast(shared)booktext, null, GWidth, GHeight));
296 refresh();
299 void formatComplete (ref ReformatWorkComplete w) {
300 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
302 auto lt = cast(LayText)w.laytext;
303 scope(exit) if (lt) lt.freeMemory();
304 w.laytext = null;
306 BookMetadata meta = cast(BookMetadata)w.meta;
307 w.meta = null;
309 BookText bt = cast(BookText)w.booktext;
310 w.booktext = null;
311 if (bt !is booktext) {
312 booktext = bt;
313 bookmeta = meta;
314 firstFormat = true;
315 sdwindow.title = booktext.title~" \xe2\x80\x94 "~booktext.authorFirst~" "~booktext.authorLast;
316 } else if (bookmeta is null) {
317 bookmeta = meta;
320 if (doQuit || formatWorks <= 0) return;
321 --formatWorks;
323 if (w.w != GWidth) {
324 if (formatWorks == 0) reformat();
325 return;
328 if (formatWorks != 0) return;
329 freeShipModel();
331 uint widx = 0;
332 if (!firstFormat && laytext !is null && laytext.lineCount) {
333 auto lidx = laytext.findLineAtY(topY);
334 if (lidx >= 0) widx = laytext.line(lidx).wstart;
336 if (laytext !is null) laytext.freeMemory();
337 laytext = lt;
338 lt = null;
339 if (firstFormat) {
340 loadState();
341 firstFormat = false;
342 doSaveCheck = false;
343 stateChanged();
344 } else {
345 goTo(widx);
347 refresh();
350 void pushPosition () {
351 if (laytext is null || laytext.lineCount == 0) return;
352 auto lidx = laytext.findLineAtY(topY);
353 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
356 void popPosition () {
357 if (posstack.length == 0) return;
358 auto widx = posstack[$-1];
359 posstack.length -= 1;
360 posstack.assumeSafeAppend;
361 goTo(widx);
364 void gotoSection (int sn) {
365 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
366 if (sn < 0 || sn >= bookmeta.sections.length) return;
367 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
368 if (lidx >= 0) {
369 auto newY = laytext.line(lidx).y;
370 if (newY != topY) {
371 topY = newY;
372 stateChanged();
373 refresh();
378 version(X11) sdwindow.closeQuery = delegate () { doQuit = true; };
380 void closeWindow () {
381 doSaveState(true); // forced state save
382 if (!sdwindow.closed && vg !is null) {
383 if (curImg >= 0) { vg.deleteImage(curImg); curImg = -1; }
384 if (curImgWhite >= 0) { vg.deleteImage(curImgWhite); curImgWhite = -1; }
385 freeShipModel();
386 vg.deleteGL2();
387 vg = null;
388 sdwindow.close();
392 sdwindow.visibleForTheFirstTime = delegate () {
393 sdwindow.setAsCurrentOpenGlContext(); // make this window active
394 sdwindow.vsync = false;
395 //sdwindow.useGLFinish = false;
396 //glbindLoadFunctions();
398 try {
399 uint flags = NVG_DEBUG;
400 if (flagNanoAA) flags |= NVG_ANTIALIAS;
401 if (flagNanoSS) flags |= NVG_STENCIL_STROKES;
402 vg = createGL2NVG(flags);
403 if (vg is null) {
404 import std.stdio;
405 writeln("Could not init nanovg.");
406 assert(0);
407 //sdwindow.close();
409 loadFonts(vg);
410 curImg = createCursorImage(vg);
411 curImgWhite = createCursorImage(vg, true);
412 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
413 } catch (Exception e) {
414 import std.stdio : stderr;
415 stderr.writeln("ERROR: ", e.msg);
416 doQuit = true;
417 return;
420 reformat();
422 refresh();
423 sdwindow.redrawOpenGlScene();
424 refresh();
427 void relayout (bool forced=false) {
428 if (laytext !is null) {
429 uint widx;
430 auto lidx = laytext.findLineAtY(topY);
431 if (lidx >= 0) widx = laytext.line(lidx).wstart;
432 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
433 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
435 import core.time;
436 auto stt = MonoTime.currTime;
437 laytext.relayout(maxWidth, forced);
438 auto ett = MonoTime.currTime-stt;
439 writeln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
441 goTo(widx);
442 refresh();
446 sdwindow.windowResized = delegate (int w, int h) {
447 //writeln("w=", w, "; h=", h);
448 //if (w < MinWinWidth) w = MinWinWidth;
449 //if (h < MinWinHeight) h = MinWinHeight;
450 glViewport(0, 0, w, h);
451 GWidth = w;
452 GHeight = h;
453 textHeight = GHeight-8;
454 //reformat();
455 relayout();
458 void drawShipName () {
459 if (shipModel is null || shipModel.name.length == 0) return;
460 vg.fontFaceId(uiFont);
461 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
462 vg.fontSize(fsizeUI);
463 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
464 float h = BND_WIDGET_HEIGHT+8;
465 float mx = (GWidth-w)/2.0;
466 float my = (GHeight-h)-8;
467 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
468 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
469 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
470 my -= BND_WIDGET_HEIGHT+16;
471 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
472 mx = (GWidth-w)/2.0;
473 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
474 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
478 void createSectionMenu () {
479 closeMenu();
480 //writeln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
481 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
482 currPopup = new PopupMenu(vg, "Sections"d, () {
483 dstring[] items;
484 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
485 return items;
487 //writeln(currPopup.items.length);
488 // find current section
489 currPopup.curItemIdx = 0;
490 auto lidx = laytext.findLineAtY(topY);
491 if (lidx >= 0 && bookmeta.sections.length > 0) {
492 foreach (immutable sidx, const ref sc; bookmeta.sections) {
493 auto sline = laytext.findLineWithWord(sc.wordidx);
494 if (sline >= 0 && lidx >= sline) currPopup.curItemIdx = cast(int)sidx;
497 onPopupSelect = (int item) { gotoSection(item); };
500 void createQuitMenu (bool wantYes) {
501 closeMenu();
502 currPopup = new PopupMenu(vg, "Quit?"d, () {
503 return ["Yes"d, "No"d];
505 currPopup.curItemIdx = (wantYes ? 0 : 1);
506 onPopupSelect = (int item) { if (item == 0) doQuit = true; };
509 void createRecentMenu () {
510 closeMenu();
511 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
512 if (recentFiles.length == 0) { freeShipModel(); return; }
513 currPopup = new PopupMenu(vg, "Recent files"d, () {
514 import std.conv : to;
515 dstring[] res;
516 foreach (const ref BookInfo bi; recentFiles) {
517 string s = bi.title;
518 if (bi.seqname.length) {
519 s ~= " (";
520 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
521 //writeln(bi.seqname);
522 s ~= bi.seqname;
523 s ~= ")";
525 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
526 res ~= s.to!dstring;
528 return res;
530 currPopup.curItemIdx = cast(int)recentFiles.length-1;
531 onPopupSelect = (int item) {
532 newBookFileName = recentFiles[item].diskfile;
533 popupNoShipKill = true;
537 bool menuKey (KeyEvent event) {
538 if (formatWorks != 0) return false;
539 if (!inMenu) return false;
540 if (inGalaxyMap) return false;
541 if (!event.pressed) return false;
542 auto res = currPopup.onKey(event);
543 if (res == PopupMenu.Close) {
544 closeMenu();
545 freeShipModel();
546 refresh();
547 } else if (res >= 0) {
548 if (onPopupSelect !is null) onPopupSelect(res);
549 closeMenu();
550 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
551 refresh();
553 return true;
556 bool menuMouse (MouseEvent event) {
557 if (formatWorks != 0) return false;
558 if (!inMenu) return false;
559 if (inGalaxyMap) return false;
560 auto res = currPopup.onMouse(event);
561 if (res == PopupMenu.Close) {
562 closeMenu();
563 freeShipModel();
564 refresh();
565 } else if (res >= 0) {
566 if (onPopupSelect !is null) onPopupSelect(res);
567 closeMenu();
568 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
569 refresh();
571 return true;
574 bool readerKey (KeyEvent event) {
575 if (formatWorks != 0) return false;
576 if (!event.pressed) {
577 switch (event.key) {
578 case Key.Up: arrowDir = 0; return true;
579 case Key.Down: arrowDir = 0; return true;
580 default:
582 return false;
584 switch (event.key) {
585 case Key.Space:
586 if (event.modifierState&ModifierState.shift) {
587 //goto case Key.PageUp;
588 hardScrollBy(toMove); toMove = 0;
589 scrollBy(-textHeight/3*2);
590 } else {
591 //goto case Key.PageDown;
592 hardScrollBy(toMove); toMove = 0;
593 scrollBy(textHeight/3*2);
595 break;
596 case Key.Backspace:
597 popPosition();
598 break;
599 case Key.PageUp:
600 hardScrollBy(toMove); toMove = 0;
601 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
602 break;
603 case Key.PageDown:
604 hardScrollBy(toMove); toMove = 0;
605 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
606 break;
607 case Key.Up:
608 //scrollBy(-8);
609 arrowDir = -1;
610 break;
611 case Key.Down:
612 //scrollBy(8);
613 arrowDir = 1;
614 break;
615 case Key.H:
616 goHome();
617 break;
618 default:
620 return true;
623 bool controlKey (KeyEvent event) {
624 if (!event.pressed) return false;
625 switch (event.key) {
626 case Key.Escape:
627 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
628 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
629 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
630 ensureShipModel();
631 createQuitMenu(true);
632 refresh();
633 return true;
634 case Key.P: if (event.modifierState == ModifierState.ctrl) { fpsVisible = !fpsVisible; refresh(); return true; } break;
635 case Key.I: if (event.modifierState == ModifierState.ctrl) { interAllowed = !interAllowed; refresh(); return true; } break;
636 case Key.N: if (event.modifierState == ModifierState.ctrl) { sbLeft = !sbLeft; refresh(); return true; } break;
637 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
638 case Key.E:
639 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
640 if (!inMenu) {
641 showShip = !showShip;
642 if (showShip) ensureShipModel(); else freeShipModel();
643 refresh();
645 return true;
647 break;
648 case Key.Q: if (event.modifierState == ModifierState.ctrl) { doQuit = true; return true; } break;
649 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
650 case Key.S:
651 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
652 ensureShipModel();
653 createSectionMenu();
654 refresh();
656 break;
657 case Key.L:
658 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
659 ensureShipModel();
660 createRecentMenu();
661 refresh();
663 break;
664 case Key.M:
665 if (!inMenu && !showShip) {
666 inGalaxyMap = !inGalaxyMap;
667 refresh();
669 break;
670 case Key.R:
671 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
672 break;
673 case Key.Home: if (showShip) { setShip(0); return true; } break;
674 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
675 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
676 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
677 default:
679 return showShip;
682 static int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
683 static int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
685 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
686 int endY () { pragma(inline, true); return startY+textHeight-1; }
688 sdwindow.redrawOpenGlScene = delegate () {
689 if (doQuit) return;
690 // timers
691 prevt = curt;
692 curt = MonoTime.currTime;
693 secs = cast(double)((curt-stt).total!"msecs")/1000.0;
694 dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
696 //glClearColor(0, 0, 0, 0);
697 //glClearColor(0.18, 0.18, 0.18, 0);
698 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
699 glClear(glNVGClearFlags|EliteModel.glClearFlags);
701 refreshed();
702 needRedrawFlag = (fps !is null && fpsVisible);
703 if (fps !is null) fps.update(dt);
704 if (vg is null) return;
706 scope(exit) vg.releaseImages();
707 vg.beginFrame(GWidth, GHeight, 1);
708 drawIfs(vg);
709 // draw scrollbar
711 float curHeight = (laytext !is null ? topY : 0);
712 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
713 if (th <= 0) { curHeight = 0; th = 1; }
714 float sz = cast(float)(GHeight-4)/th;
715 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
716 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
717 vg.bndScrollBar(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
719 if (laytext is null) {
720 if (shipModel is null) {
721 vg.beginPath();
722 vg.fillColor(colorText);
723 int drawY = (GHeight-textHeight)/2;
724 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
725 vg.fontFaceId(textFont);
726 vg.fontSize(fsizeText);
727 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
728 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
729 vg.fill();
732 // draw text page
733 if (laytext !is null && laytext.lineCount) {
734 vg.beginPath();
735 vg.fillColor(colorText);
736 int drawY = startY;
737 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
738 //FIXME: not GHeight!
739 int lidx = laytext.findLineAtY(topY);
740 if (lidx >= 0 && lidx < laytext.lineCount) {
741 drawY -= topY-laytext.line(lidx).y;
742 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
743 LayFontStyle lastStyle;
744 int startx = startX;
745 while (lidx < laytext.lineCount && drawY < GHeight) {
746 auto ln = laytext.line(lidx);
747 foreach (ref LayWord w; laytext.lineWords(lidx)) {
748 if (lastStyle != w.style) {
749 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
750 vg.fontSize(w.style.fontsize);
751 auto c = NVGColor(w.style.color);
752 vg.fillColor(c);
753 //vg.strokeColor(c);
754 lastStyle = w.style;
756 // line highlighting
757 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
758 auto oid = w.objectIdx;
759 if (oid >= 0) {
760 vg.fill();
761 laytext.objects[oid].draw(vg, startx+w.x, drawY+ln.h+ln.desc);
762 vg.beginPath();
763 } else {
764 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
766 //TODO: draw lines over whitespace
767 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
768 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/2, w.w, 1);
769 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
770 version(debug_draw) {
771 vg.fill();
772 vg.beginPath();
773 vg.strokeWidth(1);
774 vg.strokeColor(nvgRGB(0, 0, 255));
775 vg.rect(startx+w.x, drawY, w.w, w.h);
776 vg.stroke();
777 vg.beginPath();
779 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
781 drawY += ln.h;
782 ++lidx;
785 vg.fill();
787 // dim text
788 if (!showShip) {
789 if (colorDim.a != 1) {
790 //vg.scissor(0, 0, GWidth, GHeight);
791 vg.resetScissor;
792 vg.beginPath();
793 vg.fillColor(colorDim);
794 vg.rect(0, 0, GWidth, GHeight);
795 vg.fill();
798 // dim more if menu is active
799 if (inMenu || showShip || formatWorks != 0) {
800 //vg.scissor(0, 0, GWidth, GHeight);
801 vg.resetScissor;
802 vg.beginPath();
803 //vg.globalAlpha(0.5);
804 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
805 vg.rect(0, 0, GWidth, GHeight);
806 vg.fill();
808 if (shipModel !is null) {
809 vg.endFrame();
810 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
811 zz += 10;
812 lightsClear();
814 lightAdd(
815 0, 60, 60,
816 1.0, 0.0, 0.0
819 lightAdd(
820 //0, 0, -60,
821 0, 0, -zz,
822 1.0, 1.0, 1.0
824 drawModel(shipAngle, shipModel);
825 vg.beginFrame(GWidth, GHeight, 1);
826 drawShipName();
827 //vg.endFrame();
829 if (formatWorks == 0) {
830 if (inMenu) {
831 //vg.beginFrame(GWidth, GHeight, 1);
832 //vg.scissor(0, 0, GWidth, GHeight);
833 vg.resetScissor;
834 currPopup.draw();
835 //vg.endFrame();
838 if (fps !is null && fpsVisible) {
839 //vg.beginFrame(GWidth, GHeight, 1);
840 //vg.scissor(0, 0, GWidth, GHeight);
841 vg.resetScissor;
842 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
843 //vg.endFrame();
845 if (inGalaxyMap) drawGalaxy(vg);
846 // mouse cursor
847 if (curImg >= 0 && !mouseHidden) {
848 int w, h;
849 //vg.beginFrame(GWidth, GHeight, 1);
850 vg.beginPath();
851 //vg.scissor(0, 0, GWidth, GHeight);
852 vg.resetScissor;
853 vg.imageSize(curImg, &w, &h);
854 if (!mouseHigh) {
855 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
856 } else {
857 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
859 vg.rect(mouseX, mouseY, w, h);
860 vg.fill();
862 if (mouseHigh) {
863 vg.beginPath();
864 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
865 vg.rect(mouseX, mouseY, w, h);
866 vg.fill();
869 //vg.endFrame();
871 vg.endFrame();
874 void processThreads () {
875 ReformatWorkComplete wd;
876 for (;;) {
877 bool workDone = false;
878 auto res = receiveTimeout(Duration.zero,
879 (QuitWork w) {
880 formatWorks = -1;
882 (ReformatWorkComplete w) {
883 wd = w;
884 workDone = true;
887 if (!res) { assert(!workDone); break; }
888 if (workDone) { workDone = false; formatComplete(wd); }
892 sdwindow.eventLoop(1000/35,
893 delegate () {
894 processThreads();
895 if (sdwindow.closed) return;
896 if (doQuit) { closeWindow(); return; }
897 auto ctt = MonoTime.currTime;
898 // smooth scrolling
899 if (formatWorks == 0) {
900 enum Delta = 92*2;
901 if (toMove < 0) {
902 int sc = -toMove;
903 if (sc > Delta) sc = Delta;
904 hardScrollBy(-sc);
905 toMove += sc;
906 nextFadeTime = ctt+500.msecs;
907 } else if (toMove > 0) {
908 int sc = toMove;
909 if (sc > Delta) sc = Delta;
910 hardScrollBy(sc);
911 toMove -= sc;
912 nextFadeTime = ctt+500.msecs;
913 } else if (arrowDir) {
914 hardScrollBy(arrowDir*6*2);
916 // highlight fading
917 if (newYFade) {
918 if (ctt >= nextFadeTime) {
919 if ((newYAlpha -= 0.1) <= 0) {
920 newYFade = false;
921 } else {
922 nextFadeTime = ctt+25.msecs;
924 refresh();
928 // interference processing
929 if (ctt >= nextIfTime) {
930 import std.random : uniform;
931 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
932 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
934 if (processIfs()) refresh();
935 // ship rotation
936 if (shipModel !is null) {
937 shipAngle -= 1;
938 if (shipAngle < 359) shipAngle += 360;
939 refresh();
941 // mouse autohide
942 if (!mouseHidden && !mouseHigh) {
943 if ((ctt-lastMMove).total!"msecs" > 2500) {
944 mouseHidden = true;
945 refresh();
948 if (needRedraw) sdwindow.redrawOpenGlSceneNow();
949 doSaveState();
950 // load new book?
951 if (newBookFileName.length && formatWorks == 0) {
952 doSaveState(true); // forced state save
953 closeMenu();
954 ensureShipModel();
955 loadAndFormat(newBookFileName);
956 newBookFileName = null;
957 sdwindow.redrawOpenGlSceneNow();
958 //refresh();
961 delegate (KeyEvent event) {
962 if (sdwindow.closed) return;
963 if (event.key == Key.PadEnter) event.key = Key.Enter;
964 if ((event.modifierState&ModifierState.numLock) == 0) {
965 switch (event.key) {
966 case Key.Pad0: event.key = Key.Insert; break;
967 case Key.PadDot: event.key = Key.Delete; break;
968 case Key.Pad1: event.key = Key.End; break;
969 case Key.Pad2: event.key = Key.Down; break;
970 case Key.Pad3: event.key = Key.PageDown; break;
971 case Key.Pad4: event.key = Key.Left; break;
972 case Key.Pad6: event.key = Key.Right; break;
973 case Key.Pad7: event.key = Key.Home; break;
974 case Key.Pad8: event.key = Key.Up; break;
975 case Key.Pad9: event.key = Key.PageUp; break;
976 //case Key.PadEnter: event.key = Key.Enter; break;
977 default:
980 if (controlKey(event)) return;
981 if (menuKey(event)) return;
982 if (readerKey(event)) return;
984 delegate (MouseEvent event) {
985 int linkAt (int msx, int msy) {
986 if (laytext !is null && bookmeta !is null) {
987 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
988 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
989 if (widx >= 0) {
990 //writeln("word at (", msx-startX, ",", msy-startY, "): ", widx);
991 auto w = laytext.wordByIndex(widx);
992 while (widx >= 0) {
993 //writeln("word #", widx, "; href=", w.style.href);
994 if (!w.style.href) break;
995 if (auto hr = w.wordNum in bookmeta.hrefs) {
996 dstring href = hr.name;
997 if (href.length > 1 && href[0] == '#') {
998 href = href[1..$];
999 foreach (const ref id; bookmeta.ids) {
1000 if (id.name == href) {
1001 //pushPosition();
1002 //goTo(id.wordidx);
1003 return id.wordidx;
1006 //writeln("id '", hr.name, "' not found!");
1007 return -1;
1010 --widx;
1011 --w;
1016 return -1;
1019 lastMMove = MonoTime.currTime;
1020 if (mouseHidden) {
1021 mouseHidden = false;
1022 refresh();
1024 if (mouseX != event.x || mouseY != event.y) {
1025 mouseX = event.x;
1026 mouseY = event.y;
1027 refresh();
1029 if (!menuMouse(event) && !showShip) {
1030 if (event.type == MouseEventType.buttonPressed) {
1031 switch (event.button) {
1032 case MouseButton.wheelUp: hardScrollBy(-42); break;
1033 case MouseButton.wheelDown: hardScrollBy(42); break;
1034 case MouseButton.left:
1035 auto wid = linkAt(event.x, event.y);
1036 if (wid >= 0) {
1037 pushPosition();
1038 goTo(wid);
1040 break;
1041 case MouseButton.right:
1042 popPosition();
1043 break;
1044 default:
1048 mouseHigh = (linkAt(event.x, event.y) >= 0);
1050 delegate (dchar ch) {
1051 //if (ch == 'q') { doQuit = true; return; }
1054 closeWindow();
1056 childTid.send(QuitWork());
1057 while (formatWorks >= 0) processThreads();
1061 // ////////////////////////////////////////////////////////////////////////// //
1062 void main (string[] args) {
1063 import std.path;
1065 universe = Galaxy(0);
1067 if (args.length == 1) {
1068 try {
1069 string lnn;
1070 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
1071 if (!line.isComment) lnn = line;
1073 if (lnn.length) args ~= lnn;
1074 } catch (Exception) {}
1076 if (args.length == 1) assert(0, "no filename");
1078 readConfig();
1080 run(args[1]);