removed unused time variable
[xreader.git] / xreader.d
blob2138b245d915c883af4b572740b9187a61238db6
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 LayText laytext;
82 int textHeight = GHeight-8;
83 bool doSaveCheck = false;
84 MonoTime nextSaveTime;
86 MonoTime nextIfTime = MonoTime.currTime;
88 BookInfo[] recentFiles;
89 BookMetadata bookmeta;
91 int toMove = 0; // for smooth scroller
92 int topY = 0;
93 int arrowDir = 0;
95 int newYLine = -1; // index
96 float newYAlpha;
97 bool newYFade = false;
98 MonoTime nextFadeTime;
100 int curImg = -1, curImgWhite = -1;
101 int mouseX = -666, mouseY = -666;
102 bool mouseHigh = false;
103 bool mouseHidden = false;
104 auto lastMMove = MonoTime.currTime;
106 bool needRedrawFlag = true;
108 bool firstFormat = true;
110 bool inGalaxyMap = false;
111 PopupMenu currPopup;
112 void delegate (int item) onPopupSelect;
113 int shipModelIndex = 0;
114 bool popupNoShipKill = false;
116 void refresh () { needRedrawFlag = true; }
117 void refreshed () { needRedrawFlag = false; }
119 bool needRedraw () {
120 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
123 @property bool inMenu () { return (currPopup !is null); }
124 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; }
126 auto childTid = spawn(&reformatThreadFn, thisTid);
127 childTid.setMaxMailboxSize(128, OnCrowding.block);
128 thisTid.setMaxMailboxSize(128, OnCrowding.block);
130 void setShip (int idx) {
131 if (eliteShipFiles.length) {
132 import core.memory : GC;
133 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
134 if (idx < 0) idx = 0;
135 if (shipModelIndex == idx && shipModel !is null) return;
136 // remove old ship
137 shipModelIndex = idx;
138 if (shipModel !is null) {
139 shipModel.glUnload();
140 shipModel.freeData();
141 shipModel.destroy;
142 shipModel = null;
144 GC.collect();
145 // load new ship
146 try {
147 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
148 shipModel.glUpload();
149 shipModel.freeImages();
150 } catch (Exception e) {}
151 //shipModel = eliteShips[shipModelIndex];
152 GC.collect();
156 void ensureShipModel () {
157 if (eliteShipFiles.length && shipModel is null) {
158 import std.random : uniform;
159 setShip(uniform!"[)"(0, eliteShipFiles.length));
163 void freeShipModel () {
164 if (!showShip && !inMenu && shipModel !is null) {
165 import core.memory : GC;
166 shipModel.glUnload();
167 shipModel.freeData();
168 shipModel.destroy;
169 shipModel = null;
170 GC.collect();
174 void doSaveState (bool forced=false) {
175 if (!forced) {
176 if (formatWorks != 0) return;
177 if (!doSaveCheck) return;
178 auto ct = MonoTime.currTime;
179 if (ct < nextSaveTime) return;
180 } else {
181 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
183 try {
184 auto fo = VFile(stateFileName, "w");
185 if (laytext !is null && laytext.lineCount) {
186 auto lnum = laytext.findLineAtY(topY);
187 if (lnum >= 0) fo.writeln("wordindex=", laytext.line(lnum).wstart);
189 } catch (Exception) {}
190 doSaveCheck = false;
193 void stateChanged () {
194 if (!doSaveCheck) {
195 doSaveCheck = true;
196 nextSaveTime = MonoTime.currTime+10.seconds;
200 void hardScrollBy (int delta) {
201 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
202 int oldY = topY;
203 topY += delta;
204 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
205 if (topY < 0) topY = 0;
206 if (topY != oldY) {
207 stateChanged();
208 refresh();
212 void scrollBy (int delta) {
213 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
214 toMove += delta;
215 newYFade = true;
216 newYAlpha = 1;
217 if (delta < 0) {
218 // scrolling up, mark top line
219 newYLine = laytext.findLineAtY(topY);
220 } else {
221 // scrolling down, mark bottom line
222 newYLine = laytext.findLineAtY(topY+textHeight-2);
224 version(none) {
225 import std.stdio;
226 writeln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
228 if (newYLine < 0) newYFade = false;
231 void goHome () {
232 if (laytext is null) return;
233 if (topY != 0) {
234 topY = 0;
235 stateChanged();
236 refresh();
240 void goTo (uint widx) {
241 if (laytext is null) return;
242 auto lidx = laytext.findLineWithWord(widx);
243 if (lidx != -1) {
244 assert(lidx < laytext.lineCount);
245 toMove = 0;
246 if (topY != laytext.line(lidx).y) {
247 topY = laytext.line(lidx).y;
248 stateChanged();
249 refresh();
251 version(none) {
252 import std.stdio;
253 writeln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
254 auto lnum = laytext.findLineAtY(topY);
255 writeln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
260 void loadState () {
261 try {
262 int widx = -1;
263 xiniParse(VFile(stateFileName),
264 "wordindex", &widx,
266 if (widx >= 0) goTo(widx);
267 } catch (Exception) {}
270 void loadAndFormat (string filename) {
271 assert(formatWorks <= 0);
272 booktext = null;
273 laytext = null;
274 newYLine = -1;
275 //formatWorks = -1; //FIXME
276 firstFormat = true;
277 newYFade = false;
278 toMove = 0;
279 recentFiles = null;
280 arrowDir = 0;
281 //sdwindow.redrawOpenGlSceneNow();
282 //booktext = loadBook(newBookFileName);
283 //newBookFileName = null;
284 //reformat();
285 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
286 formatWorks = 1;
287 //writeln("*** loading new book: '", filename, "'");
288 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
289 refresh();
292 void reformat () {
293 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
294 childTid.send(ReformatWork(cast(shared)booktext, null, GWidth, GHeight));
295 refresh();
298 void formatComplete (ref ReformatWorkComplete w) {
299 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
301 auto lt = cast(LayText)w.laytext;
302 scope(exit) if (lt) lt.freeMemory();
303 w.laytext = null;
305 BookMetadata meta = cast(BookMetadata)w.meta;
306 w.meta = null;
308 BookText bt = cast(BookText)w.booktext;
309 w.booktext = null;
310 if (bt !is booktext) {
311 booktext = bt;
312 bookmeta = meta;
313 firstFormat = true;
314 sdwindow.title = booktext.title~" \xe2\x80\x94 "~booktext.authorFirst~" "~booktext.authorLast;
315 } else if (bookmeta is null) {
316 bookmeta = meta;
319 if (doQuit || formatWorks <= 0) return;
320 --formatWorks;
322 if (w.w != GWidth) {
323 if (formatWorks == 0) reformat();
324 return;
327 if (formatWorks != 0) return;
328 freeShipModel();
330 uint widx = 0;
331 if (!firstFormat && laytext !is null && laytext.lineCount) {
332 auto lidx = laytext.findLineAtY(topY);
333 if (lidx >= 0) widx = laytext.line(lidx).wstart;
335 if (laytext !is null) laytext.freeMemory();
336 laytext = lt;
337 lt = null;
338 if (firstFormat) {
339 loadState();
340 firstFormat = false;
341 doSaveCheck = false;
342 stateChanged();
343 } else {
344 goTo(widx);
346 refresh();
349 void pushPosition () {
350 if (laytext is null || laytext.lineCount == 0) return;
351 auto lidx = laytext.findLineAtY(topY);
352 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
355 void popPosition () {
356 if (posstack.length == 0) return;
357 auto widx = posstack[$-1];
358 posstack.length -= 1;
359 posstack.assumeSafeAppend;
360 goTo(widx);
363 void gotoSection (int sn) {
364 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
365 if (sn < 0 || sn >= bookmeta.sections.length) return;
366 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
367 if (lidx >= 0) {
368 auto newY = laytext.line(lidx).y;
369 if (newY != topY) {
370 topY = newY;
371 stateChanged();
372 refresh();
377 version(X11) sdwindow.closeQuery = delegate () { doQuit = true; };
379 void closeWindow () {
380 doSaveState(true); // forced state save
381 if (!sdwindow.closed && vg !is null) {
382 if (curImg >= 0) { vg.deleteImage(curImg); curImg = -1; }
383 if (curImgWhite >= 0) { vg.deleteImage(curImgWhite); curImgWhite = -1; }
384 freeShipModel();
385 vg.deleteGL2();
386 vg = null;
387 sdwindow.close();
391 sdwindow.visibleForTheFirstTime = delegate () {
392 sdwindow.setAsCurrentOpenGlContext(); // make this window active
393 sdwindow.vsync = false;
394 //sdwindow.useGLFinish = false;
395 //glbindLoadFunctions();
397 try {
398 uint flags = NVG_DEBUG;
399 if (flagNanoAA) flags |= NVG_ANTIALIAS;
400 if (flagNanoSS) flags |= NVG_STENCIL_STROKES;
401 vg = createGL2NVG(flags);
402 if (vg is null) {
403 import std.stdio;
404 writeln("Could not init nanovg.");
405 assert(0);
406 //sdwindow.close();
408 loadFonts(vg);
409 curImg = createCursorImage(vg);
410 curImgWhite = createCursorImage(vg, true);
411 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
412 } catch (Exception e) {
413 import std.stdio : stderr;
414 stderr.writeln("ERROR: ", e.msg);
415 doQuit = true;
416 return;
419 reformat();
421 refresh();
422 sdwindow.redrawOpenGlScene();
423 refresh();
426 void relayout (bool forced=false) {
427 if (laytext !is null) {
428 uint widx;
429 auto lidx = laytext.findLineAtY(topY);
430 if (lidx >= 0) widx = laytext.line(lidx).wstart;
431 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
432 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
434 import core.time;
435 auto stt = MonoTime.currTime;
436 laytext.relayout(maxWidth, forced);
437 auto ett = MonoTime.currTime-stt;
438 writeln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
440 goTo(widx);
441 refresh();
445 sdwindow.windowResized = delegate (int w, int h) {
446 //writeln("w=", w, "; h=", h);
447 //if (w < MinWinWidth) w = MinWinWidth;
448 //if (h < MinWinHeight) h = MinWinHeight;
449 glViewport(0, 0, w, h);
450 GWidth = w;
451 GHeight = h;
452 textHeight = GHeight-8;
453 //reformat();
454 relayout();
457 void drawShipName () {
458 if (shipModel is null || shipModel.name.length == 0) return;
459 vg.fontFaceId(uiFont);
460 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
461 vg.fontSize(fsizeUI);
462 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
463 float h = BND_WIDGET_HEIGHT+8;
464 float mx = (GWidth-w)/2.0;
465 float my = (GHeight-h)-8;
466 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
467 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
468 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
469 my -= BND_WIDGET_HEIGHT+16;
470 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
471 mx = (GWidth-w)/2.0;
472 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
473 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
477 void createSectionMenu () {
478 closeMenu();
479 //writeln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
480 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
481 currPopup = new PopupMenu(vg, "Sections"d, () {
482 dstring[] items;
483 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
484 return items;
486 //writeln(currPopup.items.length);
487 // find current section
488 currPopup.curItemIdx = 0;
489 auto lidx = laytext.findLineAtY(topY);
490 if (lidx >= 0 && bookmeta.sections.length > 0) {
491 foreach (immutable sidx, const ref sc; bookmeta.sections) {
492 auto sline = laytext.findLineWithWord(sc.wordidx);
493 if (sline >= 0 && lidx >= sline) currPopup.curItemIdx = cast(int)sidx;
496 onPopupSelect = (int item) { gotoSection(item); };
499 void createQuitMenu (bool wantYes) {
500 closeMenu();
501 currPopup = new PopupMenu(vg, "Quit?"d, () {
502 return ["Yes"d, "No"d];
504 currPopup.curItemIdx = (wantYes ? 0 : 1);
505 onPopupSelect = (int item) { if (item == 0) doQuit = true; };
508 void createRecentMenu () {
509 closeMenu();
510 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
511 if (recentFiles.length == 0) { freeShipModel(); return; }
512 currPopup = new PopupMenu(vg, "Recent files"d, () {
513 import std.conv : to;
514 dstring[] res;
515 foreach (const ref BookInfo bi; recentFiles) {
516 string s = bi.title;
517 if (bi.seqname.length) {
518 s ~= " (";
519 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
520 //writeln(bi.seqname);
521 s ~= bi.seqname;
522 s ~= ")";
524 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
525 res ~= s.to!dstring;
527 return res;
529 currPopup.curItemIdx = cast(int)recentFiles.length-1;
530 onPopupSelect = (int item) {
531 newBookFileName = recentFiles[item].diskfile;
532 popupNoShipKill = true;
536 bool menuKey (KeyEvent event) {
537 if (formatWorks != 0) return false;
538 if (!inMenu) return false;
539 if (inGalaxyMap) return false;
540 if (!event.pressed) return false;
541 auto res = currPopup.onKey(event);
542 if (res == PopupMenu.Close) {
543 closeMenu();
544 freeShipModel();
545 refresh();
546 } else if (res >= 0) {
547 if (onPopupSelect !is null) onPopupSelect(res);
548 closeMenu();
549 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
550 refresh();
552 return true;
555 bool menuMouse (MouseEvent event) {
556 if (formatWorks != 0) return false;
557 if (!inMenu) return false;
558 if (inGalaxyMap) return false;
559 auto res = currPopup.onMouse(event);
560 if (res == PopupMenu.Close) {
561 closeMenu();
562 freeShipModel();
563 refresh();
564 } else if (res >= 0) {
565 if (onPopupSelect !is null) onPopupSelect(res);
566 closeMenu();
567 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
568 refresh();
570 return true;
573 bool readerKey (KeyEvent event) {
574 if (formatWorks != 0) return false;
575 if (!event.pressed) {
576 switch (event.key) {
577 case Key.Up: arrowDir = 0; return true;
578 case Key.Down: arrowDir = 0; return true;
579 default:
581 return false;
583 switch (event.key) {
584 case Key.Space:
585 if (event.modifierState&ModifierState.shift) {
586 //goto case Key.PageUp;
587 hardScrollBy(toMove); toMove = 0;
588 scrollBy(-textHeight/3*2);
589 } else {
590 //goto case Key.PageDown;
591 hardScrollBy(toMove); toMove = 0;
592 scrollBy(textHeight/3*2);
594 break;
595 case Key.Backspace:
596 popPosition();
597 break;
598 case Key.PageUp:
599 hardScrollBy(toMove); toMove = 0;
600 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
601 break;
602 case Key.PageDown:
603 hardScrollBy(toMove); toMove = 0;
604 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
605 break;
606 case Key.Up:
607 //scrollBy(-8);
608 arrowDir = -1;
609 break;
610 case Key.Down:
611 //scrollBy(8);
612 arrowDir = 1;
613 break;
614 case Key.H:
615 goHome();
616 break;
617 default:
619 return true;
622 bool controlKey (KeyEvent event) {
623 if (!event.pressed) return false;
624 switch (event.key) {
625 case Key.Escape:
626 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
627 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
628 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
629 ensureShipModel();
630 createQuitMenu(true);
631 refresh();
632 return true;
633 case Key.P: if (event.modifierState == ModifierState.ctrl) { fpsVisible = !fpsVisible; refresh(); return true; } break;
634 case Key.I: if (event.modifierState == ModifierState.ctrl) { interAllowed = !interAllowed; refresh(); return true; } break;
635 case Key.N: if (event.modifierState == ModifierState.ctrl) { sbLeft = !sbLeft; refresh(); return true; } break;
636 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
637 case Key.E:
638 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
639 if (!inMenu) {
640 showShip = !showShip;
641 if (showShip) ensureShipModel(); else freeShipModel();
642 refresh();
644 return true;
646 break;
647 case Key.Q: if (event.modifierState == ModifierState.ctrl) { doQuit = true; return true; } break;
648 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
649 case Key.S:
650 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
651 ensureShipModel();
652 createSectionMenu();
653 refresh();
655 break;
656 case Key.L:
657 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
658 ensureShipModel();
659 createRecentMenu();
660 refresh();
662 break;
663 case Key.M:
664 if (!inMenu && !showShip) {
665 inGalaxyMap = !inGalaxyMap;
666 refresh();
668 break;
669 case Key.R:
670 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
671 break;
672 case Key.Home: if (showShip) { setShip(0); return true; } break;
673 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
674 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
675 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
676 default:
678 return showShip;
681 static int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
682 static int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
684 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
685 int endY () { pragma(inline, true); return startY+textHeight-1; }
687 sdwindow.redrawOpenGlScene = delegate () {
688 if (doQuit) return;
690 //glClearColor(0, 0, 0, 0);
691 //glClearColor(0.18, 0.18, 0.18, 0);
692 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
693 glClear(glNVGClearFlags|EliteModel.glClearFlags);
695 refreshed();
696 needRedrawFlag = (fps !is null && fpsVisible);
697 if (vg is null) return;
699 scope(exit) vg.releaseImages();
700 vg.beginFrame(GWidth, GHeight, 1);
701 drawIfs(vg);
702 // draw scrollbar
704 float curHeight = (laytext !is null ? topY : 0);
705 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
706 if (th <= 0) { curHeight = 0; th = 1; }
707 float sz = cast(float)(GHeight-4)/th;
708 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
709 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
710 vg.bndScrollBar(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
712 if (laytext is null) {
713 if (shipModel is null) {
714 vg.beginPath();
715 vg.fillColor(colorText);
716 int drawY = (GHeight-textHeight)/2;
717 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
718 vg.fontFaceId(textFont);
719 vg.fontSize(fsizeText);
720 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
721 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
722 vg.fill();
725 // draw text page
726 if (laytext !is null && laytext.lineCount) {
727 vg.beginPath();
728 vg.fillColor(colorText);
729 int drawY = startY;
730 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
731 //FIXME: not GHeight!
732 int lidx = laytext.findLineAtY(topY);
733 if (lidx >= 0 && lidx < laytext.lineCount) {
734 drawY -= topY-laytext.line(lidx).y;
735 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
736 LayFontStyle lastStyle;
737 int startx = startX;
738 while (lidx < laytext.lineCount && drawY < GHeight) {
739 auto ln = laytext.line(lidx);
740 foreach (ref LayWord w; laytext.lineWords(lidx)) {
741 if (lastStyle != w.style) {
742 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
743 vg.fontSize(w.style.fontsize);
744 auto c = NVGColor(w.style.color);
745 vg.fillColor(c);
746 //vg.strokeColor(c);
747 lastStyle = w.style;
749 // line highlighting
750 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
751 auto oid = w.objectIdx;
752 if (oid >= 0) {
753 vg.fill();
754 laytext.objects[oid].draw(vg, startx+w.x, drawY+ln.h+ln.desc);
755 vg.beginPath();
756 } else {
757 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
759 //TODO: draw lines over whitespace
760 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
761 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/2, w.w, 1);
762 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
763 version(debug_draw) {
764 vg.fill();
765 vg.beginPath();
766 vg.strokeWidth(1);
767 vg.strokeColor(nvgRGB(0, 0, 255));
768 vg.rect(startx+w.x, drawY, w.w, w.h);
769 vg.stroke();
770 vg.beginPath();
772 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
774 drawY += ln.h;
775 ++lidx;
778 vg.fill();
780 // dim text
781 if (!showShip) {
782 if (colorDim.a != 1) {
783 //vg.scissor(0, 0, GWidth, GHeight);
784 vg.resetScissor;
785 vg.beginPath();
786 vg.fillColor(colorDim);
787 vg.rect(0, 0, GWidth, GHeight);
788 vg.fill();
791 // dim more if menu is active
792 if (inMenu || showShip || formatWorks != 0) {
793 //vg.scissor(0, 0, GWidth, GHeight);
794 vg.resetScissor;
795 vg.beginPath();
796 //vg.globalAlpha(0.5);
797 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
798 vg.rect(0, 0, GWidth, GHeight);
799 vg.fill();
801 if (shipModel !is null) {
802 vg.endFrame();
803 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
804 zz += 10;
805 lightsClear();
807 lightAdd(
808 0, 60, 60,
809 1.0, 0.0, 0.0
812 lightAdd(
813 //0, 0, -60,
814 0, 0, -zz,
815 1.0, 1.0, 1.0
817 drawModel(shipAngle, shipModel);
818 vg.beginFrame(GWidth, GHeight, 1);
819 drawShipName();
820 //vg.endFrame();
822 if (formatWorks == 0) {
823 if (inMenu) {
824 //vg.beginFrame(GWidth, GHeight, 1);
825 //vg.scissor(0, 0, GWidth, GHeight);
826 vg.resetScissor;
827 currPopup.draw();
828 //vg.endFrame();
831 if (fps !is null && fpsVisible) {
832 //vg.beginFrame(GWidth, GHeight, 1);
833 //vg.scissor(0, 0, GWidth, GHeight);
834 vg.resetScissor;
835 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
836 //vg.endFrame();
838 if (inGalaxyMap) drawGalaxy(vg);
839 // mouse cursor
840 if (curImg >= 0 && !mouseHidden) {
841 int w, h;
842 //vg.beginFrame(GWidth, GHeight, 1);
843 vg.beginPath();
844 //vg.scissor(0, 0, GWidth, GHeight);
845 vg.resetScissor;
846 vg.imageSize(curImg, &w, &h);
847 if (!mouseHigh) {
848 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
849 } else {
850 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
852 vg.rect(mouseX, mouseY, w, h);
853 vg.fill();
855 if (mouseHigh) {
856 vg.beginPath();
857 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
858 vg.rect(mouseX, mouseY, w, h);
859 vg.fill();
862 //vg.endFrame();
864 vg.endFrame();
867 void processThreads () {
868 ReformatWorkComplete wd;
869 for (;;) {
870 bool workDone = false;
871 auto res = receiveTimeout(Duration.zero,
872 (QuitWork w) {
873 formatWorks = -1;
875 (ReformatWorkComplete w) {
876 wd = w;
877 workDone = true;
880 if (!res) { assert(!workDone); break; }
881 if (workDone) { workDone = false; formatComplete(wd); }
885 auto lastTimerEventTime = MonoTime.currTime;
886 bool somethingVisible = true;
888 sdwindow.visibilityChanged = delegate (bool vis) {
889 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
890 somethingVisible = vis;
893 sdwindow.eventLoop(1000/34,
894 delegate () {
895 processThreads();
896 if (sdwindow.closed) return;
897 if (doQuit) { closeWindow(); return; }
898 auto ctt = MonoTime.currTime;
901 auto spass = (ctt-lastTimerEventTime).total!"msecs";
902 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
903 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
904 lastTimerEventTime = ctt;
905 // update FPS timer
906 prevt = curt;
907 //curt = MonoTime.currTime;
908 curt = ctt;
909 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
910 auto dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
911 if (fps !is null) fps.update(dt);
914 // smooth scrolling
915 if (formatWorks == 0) {
916 enum Delta = 92*2;
917 if (toMove < 0) {
918 int sc = -toMove;
919 if (sc > Delta) sc = Delta;
920 hardScrollBy(-sc);
921 toMove += sc;
922 nextFadeTime = ctt+500.msecs;
923 refresh();
924 } else if (toMove > 0) {
925 int sc = toMove;
926 if (sc > Delta) sc = Delta;
927 hardScrollBy(sc);
928 toMove -= sc;
929 nextFadeTime = ctt+500.msecs;
930 refresh();
931 } else if (arrowDir) {
932 hardScrollBy(arrowDir*6*2);
933 refresh();
935 // highlight fading
936 if (newYFade) {
937 if (ctt >= nextFadeTime) {
938 if ((newYAlpha -= 0.1) <= 0) {
939 newYFade = false;
940 } else {
941 nextFadeTime = ctt+25.msecs;
943 refresh();
947 // interference processing
948 if (ctt >= nextIfTime) {
949 import std.random : uniform;
950 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
951 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
953 if (processIfs()) refresh();
954 // ship rotation
955 if (shipModel !is null) {
956 shipAngle -= 1;
957 if (shipAngle < 359) shipAngle += 360;
958 refresh();
960 // mouse autohide
961 if (!mouseHidden && !mouseHigh) {
962 if ((ctt-lastMMove).total!"msecs" > 2500) {
963 mouseHidden = true;
964 refresh();
967 if (somethingVisible) {
968 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
969 if (/*needRedraw*/true) sdwindow.redrawOpenGlSceneNow();
970 } else {
971 refresh();
973 doSaveState();
974 // load new book?
975 if (newBookFileName.length && formatWorks == 0) {
976 doSaveState(true); // forced state save
977 closeMenu();
978 ensureShipModel();
979 loadAndFormat(newBookFileName);
980 newBookFileName = null;
981 sdwindow.redrawOpenGlSceneNow();
982 //refresh();
985 delegate (KeyEvent event) {
986 if (sdwindow.closed) return;
987 if (event.key == Key.PadEnter) event.key = Key.Enter;
988 if ((event.modifierState&ModifierState.numLock) == 0) {
989 switch (event.key) {
990 case Key.Pad0: event.key = Key.Insert; break;
991 case Key.PadDot: event.key = Key.Delete; break;
992 case Key.Pad1: event.key = Key.End; break;
993 case Key.Pad2: event.key = Key.Down; break;
994 case Key.Pad3: event.key = Key.PageDown; break;
995 case Key.Pad4: event.key = Key.Left; break;
996 case Key.Pad6: event.key = Key.Right; break;
997 case Key.Pad7: event.key = Key.Home; break;
998 case Key.Pad8: event.key = Key.Up; break;
999 case Key.Pad9: event.key = Key.PageUp; break;
1000 //case Key.PadEnter: event.key = Key.Enter; break;
1001 default:
1004 if (controlKey(event)) return;
1005 if (menuKey(event)) return;
1006 if (readerKey(event)) return;
1008 delegate (MouseEvent event) {
1009 int linkAt (int msx, int msy) {
1010 if (laytext !is null && bookmeta !is null) {
1011 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
1012 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
1013 if (widx >= 0) {
1014 //writeln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1015 auto w = laytext.wordByIndex(widx);
1016 while (widx >= 0) {
1017 //writeln("word #", widx, "; href=", w.style.href);
1018 if (!w.style.href) break;
1019 if (auto hr = w.wordNum in bookmeta.hrefs) {
1020 dstring href = hr.name;
1021 if (href.length > 1 && href[0] == '#') {
1022 href = href[1..$];
1023 foreach (const ref id; bookmeta.ids) {
1024 if (id.name == href) {
1025 //pushPosition();
1026 //goTo(id.wordidx);
1027 return id.wordidx;
1030 //writeln("id '", hr.name, "' not found!");
1031 return -1;
1034 --widx;
1035 --w;
1040 return -1;
1043 lastMMove = MonoTime.currTime;
1044 if (mouseHidden) {
1045 mouseHidden = false;
1046 refresh();
1048 if (mouseX != event.x || mouseY != event.y) {
1049 mouseX = event.x;
1050 mouseY = event.y;
1051 refresh();
1053 if (!menuMouse(event) && !showShip) {
1054 if (event.type == MouseEventType.buttonPressed) {
1055 switch (event.button) {
1056 case MouseButton.wheelUp: hardScrollBy(-42); break;
1057 case MouseButton.wheelDown: hardScrollBy(42); break;
1058 case MouseButton.left:
1059 auto wid = linkAt(event.x, event.y);
1060 if (wid >= 0) {
1061 pushPosition();
1062 goTo(wid);
1064 break;
1065 case MouseButton.right:
1066 popPosition();
1067 break;
1068 default:
1072 mouseHigh = (linkAt(event.x, event.y) >= 0);
1074 delegate (dchar ch) {
1075 //if (ch == 'q') { doQuit = true; return; }
1078 closeWindow();
1080 childTid.send(QuitWork());
1081 while (formatWorks >= 0) processThreads();
1085 // ////////////////////////////////////////////////////////////////////////// //
1086 void main (string[] args) {
1087 import std.path;
1089 universe = Galaxy(0);
1091 if (args.length == 1) {
1092 try {
1093 string lnn;
1094 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
1095 if (!line.isComment) lnn = line;
1097 if (lnn.length) args ~= lnn;
1098 } catch (Exception) {}
1100 if (args.length == 1) assert(0, "no filename");
1102 readConfig();
1104 run(args[1]);