mouse now will fade away instead of simply disappearing
[xreader.git] / xreader.d
blobb050aa07fbb298bf2eb67e1993790958e43f49d9
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;
105 float mouseAlpha = 1.0f;
106 int mouseFadingAway = false;
108 bool needRedrawFlag = true;
110 bool firstFormat = true;
112 bool inGalaxyMap = false;
113 PopupMenu currPopup;
114 void delegate (int item) onPopupSelect;
115 int shipModelIndex = 0;
116 bool popupNoShipKill = false;
118 void refresh () { needRedrawFlag = true; }
119 void refreshed () { needRedrawFlag = false; }
121 bool needRedraw () {
122 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
125 @property bool inMenu () { return (currPopup !is null); }
126 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; }
128 auto childTid = spawn(&reformatThreadFn, thisTid);
129 childTid.setMaxMailboxSize(128, OnCrowding.block);
130 thisTid.setMaxMailboxSize(128, OnCrowding.block);
132 void setShip (int idx) {
133 if (eliteShipFiles.length) {
134 import core.memory : GC;
135 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
136 if (idx < 0) idx = 0;
137 if (shipModelIndex == idx && shipModel !is null) return;
138 // remove old ship
139 shipModelIndex = idx;
140 if (shipModel !is null) {
141 shipModel.glUnload();
142 shipModel.freeData();
143 shipModel.destroy;
144 shipModel = null;
146 GC.collect();
147 // load new ship
148 try {
149 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
150 shipModel.glUpload();
151 shipModel.freeImages();
152 } catch (Exception e) {}
153 //shipModel = eliteShips[shipModelIndex];
154 GC.collect();
158 void ensureShipModel () {
159 if (eliteShipFiles.length && shipModel is null) {
160 import std.random : uniform;
161 setShip(uniform!"[)"(0, eliteShipFiles.length));
165 void freeShipModel () {
166 if (!showShip && !inMenu && shipModel !is null) {
167 import core.memory : GC;
168 shipModel.glUnload();
169 shipModel.freeData();
170 shipModel.destroy;
171 shipModel = null;
172 GC.collect();
176 void doSaveState (bool forced=false) {
177 if (!forced) {
178 if (formatWorks != 0) return;
179 if (!doSaveCheck) return;
180 auto ct = MonoTime.currTime;
181 if (ct < nextSaveTime) return;
182 } else {
183 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
185 try {
186 auto fo = VFile(stateFileName, "w");
187 if (laytext !is null && laytext.lineCount) {
188 auto lnum = laytext.findLineAtY(topY);
189 if (lnum >= 0) fo.writeln("wordindex=", laytext.line(lnum).wstart);
191 } catch (Exception) {}
192 doSaveCheck = false;
195 void stateChanged () {
196 if (!doSaveCheck) {
197 doSaveCheck = true;
198 nextSaveTime = MonoTime.currTime+10.seconds;
202 void hardScrollBy (int delta) {
203 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
204 int oldY = topY;
205 topY += delta;
206 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
207 if (topY < 0) topY = 0;
208 if (topY != oldY) {
209 stateChanged();
210 refresh();
214 void scrollBy (int delta) {
215 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
216 toMove += delta;
217 newYFade = true;
218 newYAlpha = 1;
219 if (delta < 0) {
220 // scrolling up, mark top line
221 newYLine = laytext.findLineAtY(topY);
222 } else {
223 // scrolling down, mark bottom line
224 newYLine = laytext.findLineAtY(topY+textHeight-2);
226 version(none) {
227 import std.stdio;
228 writeln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
230 if (newYLine < 0) newYFade = false;
233 void goHome () {
234 if (laytext is null) return;
235 if (topY != 0) {
236 topY = 0;
237 stateChanged();
238 refresh();
242 void goTo (uint widx) {
243 if (laytext is null) return;
244 auto lidx = laytext.findLineWithWord(widx);
245 if (lidx != -1) {
246 assert(lidx < laytext.lineCount);
247 toMove = 0;
248 if (topY != laytext.line(lidx).y) {
249 topY = laytext.line(lidx).y;
250 stateChanged();
251 refresh();
253 version(none) {
254 import std.stdio;
255 writeln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
256 auto lnum = laytext.findLineAtY(topY);
257 writeln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
262 void loadState () {
263 try {
264 int widx = -1;
265 xiniParse(VFile(stateFileName),
266 "wordindex", &widx,
268 if (widx >= 0) goTo(widx);
269 } catch (Exception) {}
272 void loadAndFormat (string filename) {
273 assert(formatWorks <= 0);
274 booktext = null;
275 laytext = null;
276 newYLine = -1;
277 //formatWorks = -1; //FIXME
278 firstFormat = true;
279 newYFade = false;
280 toMove = 0;
281 recentFiles = null;
282 arrowDir = 0;
283 //sdwindow.redrawOpenGlSceneNow();
284 //booktext = loadBook(newBookFileName);
285 //newBookFileName = null;
286 //reformat();
287 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
288 formatWorks = 1;
289 //writeln("*** loading new book: '", filename, "'");
290 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
291 refresh();
294 void reformat () {
295 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
296 childTid.send(ReformatWork(cast(shared)booktext, null, GWidth, GHeight));
297 refresh();
300 void formatComplete (ref ReformatWorkComplete w) {
301 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
303 auto lt = cast(LayText)w.laytext;
304 scope(exit) if (lt) lt.freeMemory();
305 w.laytext = null;
307 BookMetadata meta = cast(BookMetadata)w.meta;
308 w.meta = null;
310 BookText bt = cast(BookText)w.booktext;
311 w.booktext = null;
312 if (bt !is booktext) {
313 booktext = bt;
314 bookmeta = meta;
315 firstFormat = true;
316 sdwindow.title = booktext.title~" \xe2\x80\x94 "~booktext.authorFirst~" "~booktext.authorLast;
317 } else if (bookmeta is null) {
318 bookmeta = meta;
321 if (doQuit || formatWorks <= 0) return;
322 --formatWorks;
324 if (w.w != GWidth) {
325 if (formatWorks == 0) reformat();
326 return;
329 if (formatWorks != 0) return;
330 freeShipModel();
332 uint widx = 0;
333 if (!firstFormat && laytext !is null && laytext.lineCount) {
334 auto lidx = laytext.findLineAtY(topY);
335 if (lidx >= 0) widx = laytext.line(lidx).wstart;
337 if (laytext !is null) laytext.freeMemory();
338 laytext = lt;
339 lt = null;
340 if (firstFormat) {
341 loadState();
342 firstFormat = false;
343 doSaveCheck = false;
344 stateChanged();
345 } else {
346 goTo(widx);
348 refresh();
351 void pushPosition () {
352 if (laytext is null || laytext.lineCount == 0) return;
353 auto lidx = laytext.findLineAtY(topY);
354 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
357 void popPosition () {
358 if (posstack.length == 0) return;
359 auto widx = posstack[$-1];
360 posstack.length -= 1;
361 posstack.assumeSafeAppend;
362 goTo(widx);
365 void gotoSection (int sn) {
366 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
367 if (sn < 0 || sn >= bookmeta.sections.length) return;
368 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
369 if (lidx >= 0) {
370 auto newY = laytext.line(lidx).y;
371 if (newY != topY) {
372 topY = newY;
373 stateChanged();
374 refresh();
379 version(X11) sdwindow.closeQuery = delegate () { doQuit = true; };
381 void closeWindow () {
382 doSaveState(true); // forced state save
383 if (!sdwindow.closed && vg !is null) {
384 if (curImg >= 0) { vg.deleteImage(curImg); curImg = -1; }
385 if (curImgWhite >= 0) { vg.deleteImage(curImgWhite); curImgWhite = -1; }
386 freeShipModel();
387 vg.deleteGL2();
388 vg = null;
389 sdwindow.close();
393 sdwindow.visibleForTheFirstTime = delegate () {
394 sdwindow.setAsCurrentOpenGlContext(); // make this window active
395 sdwindow.vsync = false;
396 //sdwindow.useGLFinish = false;
397 //glbindLoadFunctions();
399 try {
400 uint flags = NVG_DEBUG;
401 if (flagNanoAA) flags |= NVG_ANTIALIAS;
402 if (flagNanoSS) flags |= NVG_STENCIL_STROKES;
403 vg = createGL2NVG(flags);
404 if (vg is null) {
405 import std.stdio;
406 writeln("Could not init nanovg.");
407 assert(0);
408 //sdwindow.close();
410 loadFonts(vg);
411 curImg = createCursorImage(vg);
412 curImgWhite = createCursorImage(vg, true);
413 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
414 } catch (Exception e) {
415 import std.stdio : stderr;
416 stderr.writeln("ERROR: ", e.msg);
417 doQuit = true;
418 return;
421 reformat();
423 refresh();
424 sdwindow.redrawOpenGlScene();
425 refresh();
428 void relayout (bool forced=false) {
429 if (laytext !is null) {
430 uint widx;
431 auto lidx = laytext.findLineAtY(topY);
432 if (lidx >= 0) widx = laytext.line(lidx).wstart;
433 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
434 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
436 import core.time;
437 auto stt = MonoTime.currTime;
438 laytext.relayout(maxWidth, forced);
439 auto ett = MonoTime.currTime-stt;
440 writeln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
442 goTo(widx);
443 refresh();
447 sdwindow.windowResized = delegate (int w, int h) {
448 //writeln("w=", w, "; h=", h);
449 //if (w < MinWinWidth) w = MinWinWidth;
450 //if (h < MinWinHeight) h = MinWinHeight;
451 glViewport(0, 0, w, h);
452 GWidth = w;
453 GHeight = h;
454 textHeight = GHeight-8;
455 //reformat();
456 relayout();
459 void drawShipName () {
460 if (shipModel is null || shipModel.name.length == 0) return;
461 vg.fontFaceId(uiFont);
462 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
463 vg.fontSize(fsizeUI);
464 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
465 float h = BND_WIDGET_HEIGHT+8;
466 float mx = (GWidth-w)/2.0;
467 float my = (GHeight-h)-8;
468 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
469 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
470 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
471 my -= BND_WIDGET_HEIGHT+16;
472 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
473 mx = (GWidth-w)/2.0;
474 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
475 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
479 void createSectionMenu () {
480 closeMenu();
481 //writeln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
482 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
483 currPopup = new PopupMenu(vg, "Sections"d, () {
484 dstring[] items;
485 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
486 return items;
488 //writeln(currPopup.items.length);
489 // find current section
490 currPopup.curItemIdx = 0;
491 auto lidx = laytext.findLineAtY(topY);
492 if (lidx >= 0 && bookmeta.sections.length > 0) {
493 foreach (immutable sidx, const ref sc; bookmeta.sections) {
494 auto sline = laytext.findLineWithWord(sc.wordidx);
495 if (sline >= 0 && lidx >= sline) currPopup.curItemIdx = cast(int)sidx;
498 onPopupSelect = (int item) { gotoSection(item); };
501 void createQuitMenu (bool wantYes) {
502 closeMenu();
503 currPopup = new PopupMenu(vg, "Quit?"d, () {
504 return ["Yes"d, "No"d];
506 currPopup.curItemIdx = (wantYes ? 0 : 1);
507 onPopupSelect = (int item) { if (item == 0) doQuit = true; };
510 void createRecentMenu () {
511 closeMenu();
512 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
513 if (recentFiles.length == 0) { freeShipModel(); return; }
514 currPopup = new PopupMenu(vg, "Recent files"d, () {
515 import std.conv : to;
516 dstring[] res;
517 foreach (const ref BookInfo bi; recentFiles) {
518 string s = bi.title;
519 if (bi.seqname.length) {
520 s ~= " (";
521 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
522 //writeln(bi.seqname);
523 s ~= bi.seqname;
524 s ~= ")";
526 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
527 res ~= s.to!dstring;
529 return res;
531 currPopup.curItemIdx = cast(int)recentFiles.length-1;
532 onPopupSelect = (int item) {
533 newBookFileName = recentFiles[item].diskfile;
534 popupNoShipKill = true;
538 bool menuKey (KeyEvent event) {
539 if (formatWorks != 0) return false;
540 if (!inMenu) return false;
541 if (inGalaxyMap) return false;
542 if (!event.pressed) return false;
543 auto res = currPopup.onKey(event);
544 if (res == PopupMenu.Close) {
545 closeMenu();
546 freeShipModel();
547 refresh();
548 } else if (res >= 0) {
549 if (onPopupSelect !is null) onPopupSelect(res);
550 closeMenu();
551 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
552 refresh();
554 return true;
557 bool menuMouse (MouseEvent event) {
558 if (formatWorks != 0) return false;
559 if (!inMenu) return false;
560 if (inGalaxyMap) return false;
561 auto res = currPopup.onMouse(event);
562 if (res == PopupMenu.Close) {
563 closeMenu();
564 freeShipModel();
565 refresh();
566 } else if (res >= 0) {
567 if (onPopupSelect !is null) onPopupSelect(res);
568 closeMenu();
569 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
570 refresh();
572 return true;
575 bool readerKey (KeyEvent event) {
576 if (formatWorks != 0) return false;
577 if (!event.pressed) {
578 switch (event.key) {
579 case Key.Up: arrowDir = 0; return true;
580 case Key.Down: arrowDir = 0; return true;
581 default:
583 return false;
585 switch (event.key) {
586 case Key.Space:
587 if (event.modifierState&ModifierState.shift) {
588 //goto case Key.PageUp;
589 hardScrollBy(toMove); toMove = 0;
590 scrollBy(-textHeight/3*2);
591 } else {
592 //goto case Key.PageDown;
593 hardScrollBy(toMove); toMove = 0;
594 scrollBy(textHeight/3*2);
596 break;
597 case Key.Backspace:
598 popPosition();
599 break;
600 case Key.PageUp:
601 hardScrollBy(toMove); toMove = 0;
602 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
603 break;
604 case Key.PageDown:
605 hardScrollBy(toMove); toMove = 0;
606 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
607 break;
608 case Key.Up:
609 //scrollBy(-8);
610 arrowDir = -1;
611 break;
612 case Key.Down:
613 //scrollBy(8);
614 arrowDir = 1;
615 break;
616 case Key.H:
617 goHome();
618 break;
619 default:
621 return true;
624 bool controlKey (KeyEvent event) {
625 if (!event.pressed) return false;
626 switch (event.key) {
627 case Key.Escape:
628 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
629 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
630 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
631 ensureShipModel();
632 createQuitMenu(true);
633 refresh();
634 return true;
635 case Key.P: if (event.modifierState == ModifierState.ctrl) { fpsVisible = !fpsVisible; refresh(); return true; } break;
636 case Key.I: if (event.modifierState == ModifierState.ctrl) { interAllowed = !interAllowed; refresh(); return true; } break;
637 case Key.N: if (event.modifierState == ModifierState.ctrl) { sbLeft = !sbLeft; refresh(); return true; } break;
638 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
639 case Key.E:
640 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
641 if (!inMenu) {
642 showShip = !showShip;
643 if (showShip) ensureShipModel(); else freeShipModel();
644 refresh();
646 return true;
648 break;
649 case Key.Q: if (event.modifierState == ModifierState.ctrl) { doQuit = true; return true; } break;
650 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
651 case Key.S:
652 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
653 ensureShipModel();
654 createSectionMenu();
655 refresh();
657 break;
658 case Key.L:
659 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
660 ensureShipModel();
661 createRecentMenu();
662 refresh();
664 break;
665 case Key.M:
666 if (!inMenu && !showShip) {
667 inGalaxyMap = !inGalaxyMap;
668 refresh();
670 break;
671 case Key.R:
672 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
673 break;
674 case Key.Home: if (showShip) { setShip(0); return true; } break;
675 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
676 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
677 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
678 default:
680 return showShip;
683 static int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
684 static int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
686 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
687 int endY () { pragma(inline, true); return startY+textHeight-1; }
689 sdwindow.redrawOpenGlScene = delegate () {
690 if (doQuit) return;
692 //glClearColor(0, 0, 0, 0);
693 //glClearColor(0.18, 0.18, 0.18, 0);
694 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
695 glClear(glNVGClearFlags|EliteModel.glClearFlags);
697 refreshed();
698 needRedrawFlag = (fps !is null && fpsVisible);
699 if (vg is null) return;
701 scope(exit) vg.releaseImages();
702 vg.beginFrame(GWidth, GHeight, 1);
703 drawIfs(vg);
704 // draw scrollbar
706 float curHeight = (laytext !is null ? topY : 0);
707 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
708 if (th <= 0) { curHeight = 0; th = 1; }
709 float sz = cast(float)(GHeight-4)/th;
710 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
711 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
712 vg.bndScrollBar(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
714 if (laytext is null) {
715 if (shipModel is null) {
716 vg.beginPath();
717 vg.fillColor(colorText);
718 int drawY = (GHeight-textHeight)/2;
719 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
720 vg.fontFaceId(textFont);
721 vg.fontSize(fsizeText);
722 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
723 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
724 vg.fill();
727 // draw text page
728 if (laytext !is null && laytext.lineCount) {
729 vg.beginPath();
730 vg.fillColor(colorText);
731 int drawY = startY;
732 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
733 //FIXME: not GHeight!
734 int lidx = laytext.findLineAtY(topY);
735 if (lidx >= 0 && lidx < laytext.lineCount) {
736 drawY -= topY-laytext.line(lidx).y;
737 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
738 LayFontStyle lastStyle;
739 int startx = startX;
740 while (lidx < laytext.lineCount && drawY < GHeight) {
741 auto ln = laytext.line(lidx);
742 foreach (ref LayWord w; laytext.lineWords(lidx)) {
743 if (lastStyle != w.style) {
744 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
745 vg.fontSize(w.style.fontsize);
746 auto c = NVGColor(w.style.color);
747 vg.fillColor(c);
748 //vg.strokeColor(c);
749 lastStyle = w.style;
751 // line highlighting
752 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
753 auto oid = w.objectIdx;
754 if (oid >= 0) {
755 vg.fill();
756 laytext.objects[oid].draw(vg, startx+w.x, drawY+ln.h+ln.desc);
757 vg.beginPath();
758 } else {
759 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
761 //TODO: draw lines over whitespace
762 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
763 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/2, w.w, 1);
764 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
765 version(debug_draw) {
766 vg.fill();
767 vg.beginPath();
768 vg.strokeWidth(1);
769 vg.strokeColor(nvgRGB(0, 0, 255));
770 vg.rect(startx+w.x, drawY, w.w, w.h);
771 vg.stroke();
772 vg.beginPath();
774 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
776 drawY += ln.h;
777 ++lidx;
780 vg.fill();
782 // dim text
783 if (!showShip) {
784 if (colorDim.a != 1) {
785 //vg.scissor(0, 0, GWidth, GHeight);
786 vg.resetScissor;
787 vg.beginPath();
788 vg.fillColor(colorDim);
789 vg.rect(0, 0, GWidth, GHeight);
790 vg.fill();
793 // dim more if menu is active
794 if (inMenu || showShip || formatWorks != 0) {
795 //vg.scissor(0, 0, GWidth, GHeight);
796 vg.resetScissor;
797 vg.beginPath();
798 //vg.globalAlpha(0.5);
799 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
800 vg.rect(0, 0, GWidth, GHeight);
801 vg.fill();
803 if (shipModel !is null) {
804 vg.endFrame();
805 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
806 zz += 10;
807 lightsClear();
809 lightAdd(
810 0, 60, 60,
811 1.0, 0.0, 0.0
814 lightAdd(
815 //0, 0, -60,
816 0, 0, -zz,
817 1.0, 1.0, 1.0
819 drawModel(shipAngle, shipModel);
820 vg.beginFrame(GWidth, GHeight, 1);
821 drawShipName();
822 //vg.endFrame();
824 if (formatWorks == 0) {
825 if (inMenu) {
826 //vg.beginFrame(GWidth, GHeight, 1);
827 //vg.scissor(0, 0, GWidth, GHeight);
828 vg.resetScissor;
829 currPopup.draw();
830 //vg.endFrame();
833 if (fps !is null && fpsVisible) {
834 //vg.beginFrame(GWidth, GHeight, 1);
835 //vg.scissor(0, 0, GWidth, GHeight);
836 vg.resetScissor;
837 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
838 //vg.endFrame();
840 if (inGalaxyMap) drawGalaxy(vg);
841 // mouse cursor
842 if (curImg >= 0 && !mouseHidden) {
843 int w, h;
844 //vg.beginFrame(GWidth, GHeight, 1);
845 vg.beginPath();
846 //vg.scissor(0, 0, GWidth, GHeight);
847 vg.resetScissor;
848 vg.imageSize(curImg, &w, &h);
849 if (mouseFadingAway) {
850 mouseAlpha -= 0.1;
851 if (mouseAlpha <= 0) { mouseFadingAway = false; mouseHidden = true; }
852 } else {
853 mouseAlpha = 1.0f;
855 vg.globalAlpha(mouseAlpha);
856 if (!mouseHigh) {
857 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
858 } else {
859 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
861 vg.rect(mouseX, mouseY, w, h);
862 vg.fill();
864 if (mouseHigh) {
865 vg.beginPath();
866 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
867 vg.rect(mouseX, mouseY, w, h);
868 vg.fill();
871 //vg.endFrame();
873 vg.endFrame();
876 void processThreads () {
877 ReformatWorkComplete wd;
878 for (;;) {
879 bool workDone = false;
880 auto res = receiveTimeout(Duration.zero,
881 (QuitWork w) {
882 formatWorks = -1;
884 (ReformatWorkComplete w) {
885 wd = w;
886 workDone = true;
889 if (!res) { assert(!workDone); break; }
890 if (workDone) { workDone = false; formatComplete(wd); }
894 auto lastTimerEventTime = MonoTime.currTime;
895 bool somethingVisible = true;
897 sdwindow.visibilityChanged = delegate (bool vis) {
898 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
899 somethingVisible = vis;
902 sdwindow.eventLoop(1000/34,
903 delegate () {
904 processThreads();
905 if (sdwindow.closed) return;
906 if (doQuit) { closeWindow(); return; }
907 auto ctt = MonoTime.currTime;
910 auto spass = (ctt-lastTimerEventTime).total!"msecs";
911 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
912 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
913 lastTimerEventTime = ctt;
914 // update FPS timer
915 prevt = curt;
916 //curt = MonoTime.currTime;
917 curt = ctt;
918 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
919 auto dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
920 if (fps !is null) fps.update(dt);
923 // smooth scrolling
924 if (formatWorks == 0) {
925 enum Delta = 92*2;
926 if (toMove < 0) {
927 int sc = -toMove;
928 if (sc > Delta) sc = Delta;
929 hardScrollBy(-sc);
930 toMove += sc;
931 nextFadeTime = ctt+500.msecs;
932 refresh();
933 } else if (toMove > 0) {
934 int sc = toMove;
935 if (sc > Delta) sc = Delta;
936 hardScrollBy(sc);
937 toMove -= sc;
938 nextFadeTime = ctt+500.msecs;
939 refresh();
940 } else if (arrowDir) {
941 hardScrollBy(arrowDir*6*2);
942 refresh();
944 // highlight fading
945 if (newYFade) {
946 if (ctt >= nextFadeTime) {
947 if ((newYAlpha -= 0.1) <= 0) {
948 newYFade = false;
949 } else {
950 nextFadeTime = ctt+25.msecs;
952 refresh();
956 // interference processing
957 if (ctt >= nextIfTime) {
958 import std.random : uniform;
959 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
960 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
962 if (processIfs()) refresh();
963 // ship rotation
964 if (shipModel !is null) {
965 shipAngle -= 1;
966 if (shipAngle < 359) shipAngle += 360;
967 refresh();
969 // mouse autohide
970 if (!mouseFadingAway) {
971 if (!mouseHidden && !mouseHigh) {
972 if ((ctt-lastMMove).total!"msecs" > 2500) {
973 //mouseHidden = true;
974 mouseFadingAway = true;
975 mouseAlpha = 1.0f;
976 refresh();
980 if (somethingVisible) {
981 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
982 if (/*needRedraw*/true) sdwindow.redrawOpenGlSceneNow();
983 } else {
984 refresh();
986 doSaveState();
987 // load new book?
988 if (newBookFileName.length && formatWorks == 0) {
989 doSaveState(true); // forced state save
990 closeMenu();
991 ensureShipModel();
992 loadAndFormat(newBookFileName);
993 newBookFileName = null;
994 sdwindow.redrawOpenGlSceneNow();
995 //refresh();
998 delegate (KeyEvent event) {
999 if (sdwindow.closed) return;
1000 if (event.key == Key.PadEnter) event.key = Key.Enter;
1001 if ((event.modifierState&ModifierState.numLock) == 0) {
1002 switch (event.key) {
1003 case Key.Pad0: event.key = Key.Insert; break;
1004 case Key.PadDot: event.key = Key.Delete; break;
1005 case Key.Pad1: event.key = Key.End; break;
1006 case Key.Pad2: event.key = Key.Down; break;
1007 case Key.Pad3: event.key = Key.PageDown; break;
1008 case Key.Pad4: event.key = Key.Left; break;
1009 case Key.Pad6: event.key = Key.Right; break;
1010 case Key.Pad7: event.key = Key.Home; break;
1011 case Key.Pad8: event.key = Key.Up; break;
1012 case Key.Pad9: event.key = Key.PageUp; break;
1013 //case Key.PadEnter: event.key = Key.Enter; break;
1014 default:
1017 if (controlKey(event)) return;
1018 if (menuKey(event)) return;
1019 if (readerKey(event)) return;
1021 delegate (MouseEvent event) {
1022 int linkAt (int msx, int msy) {
1023 if (laytext !is null && bookmeta !is null) {
1024 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
1025 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
1026 if (widx >= 0) {
1027 //writeln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1028 auto w = laytext.wordByIndex(widx);
1029 while (widx >= 0) {
1030 //writeln("word #", widx, "; href=", w.style.href);
1031 if (!w.style.href) break;
1032 if (auto hr = w.wordNum in bookmeta.hrefs) {
1033 dstring href = hr.name;
1034 if (href.length > 1 && href[0] == '#') {
1035 href = href[1..$];
1036 foreach (const ref id; bookmeta.ids) {
1037 if (id.name == href) {
1038 //pushPosition();
1039 //goTo(id.wordidx);
1040 return id.wordidx;
1043 //writeln("id '", hr.name, "' not found!");
1044 return -1;
1047 --widx;
1048 --w;
1053 return -1;
1056 lastMMove = MonoTime.currTime;
1057 if (mouseHidden || mouseFadingAway) {
1058 mouseHidden = false;
1059 mouseFadingAway = false;
1060 mouseAlpha = 1.0f;
1061 refresh();
1063 if (mouseX != event.x || mouseY != event.y) {
1064 mouseX = event.x;
1065 mouseY = event.y;
1066 refresh();
1068 if (!menuMouse(event) && !showShip) {
1069 if (event.type == MouseEventType.buttonPressed) {
1070 switch (event.button) {
1071 case MouseButton.wheelUp: hardScrollBy(-42); break;
1072 case MouseButton.wheelDown: hardScrollBy(42); break;
1073 case MouseButton.left:
1074 auto wid = linkAt(event.x, event.y);
1075 if (wid >= 0) {
1076 pushPosition();
1077 goTo(wid);
1079 break;
1080 case MouseButton.right:
1081 popPosition();
1082 break;
1083 default:
1087 mouseHigh = (linkAt(event.x, event.y) >= 0);
1089 delegate (dchar ch) {
1090 //if (ch == 'q') { doQuit = true; return; }
1093 closeWindow();
1095 childTid.send(QuitWork());
1096 while (formatWorks >= 0) processThreads();
1100 // ////////////////////////////////////////////////////////////////////////// //
1101 void main (string[] args) {
1102 import std.path;
1104 universe = Galaxy(0);
1106 if (args.length == 1) {
1107 try {
1108 string lnn;
1109 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
1110 if (!line.isComment) lnn = line;
1112 if (lnn.length) args ~= lnn;
1113 } catch (Exception) {}
1115 if (args.length == 1) assert(0, "no filename");
1117 readConfig();
1119 run(args[1]);