more fixes for fb2 idiocy
[xreader.git] / xreader.d
blob2faf0be1ff7f9711d6b71b57a2ebb36daeabccf5
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.cmdcongl;
34 import iv.vfs;
35 import iv.vfs.io;
37 import xmodel;
38 import xiniz;
40 import booktext;
42 import xreadercfg;
43 import xreaderui;
44 import xreaderifs;
45 import xreaderfmt;
46 import xlayouter;
48 import eworld;
49 import ewbillboard;
51 //version = debug_draw;
54 // ////////////////////////////////////////////////////////////////////////// //
55 __gshared int oglConScale = 1;
58 // ////////////////////////////////////////////////////////////////////////// //
59 __gshared string bookFileName;
61 __gshared int formatWorks = -1;
62 __gshared NVGContext vg = null;
63 __gshared PerfGraph fps;
64 __gshared bool fpsVisible = false;
65 __gshared BookText bookText;
66 __gshared string newBookFileName;
67 __gshared uint[] posstack;
68 __gshared SimpleWindow sdwindow;
69 __gshared LayText laytext;
70 __gshared BookInfo[] recentFiles;
71 __gshared BookMetadata bookmeta;
73 __gshared int textHeight;
75 __gshared int toMove = 0; // for smooth scroller
76 __gshared int topY = 0;
77 __gshared int arrowDir = 0;
78 __gshared int arrowSpeed = 0;
80 __gshared int newYLine = -1; // index
81 __gshared float newYAlpha;
82 __gshared bool newYFade = false;
83 __gshared MonoTime nextFadeTime;
85 __gshared int curImg = -1, curImgWhite = -1;
86 __gshared int mouseX = -666, mouseY = -666;
87 __gshared bool mouseHigh = false;
88 __gshared bool mouseHidden = false;
89 __gshared MonoTime lastMMove;
90 __gshared float mouseAlpha = 1.0f;
91 __gshared int mouseFadingAway = false;
93 __gshared bool needRedrawFlag = true;
95 __gshared bool firstFormat = true;
97 __gshared bool inGalaxyMap = false;
98 __gshared PopupMenu currPopup;
99 __gshared void delegate (int item) onPopupSelect;
100 __gshared int shipModelIndex = 0;
101 __gshared bool popupNoShipKill = false;
103 __gshared bool doSaveCheck = false;
104 __gshared MonoTime nextSaveTime;
107 // ////////////////////////////////////////////////////////////////////////// //
108 void refresh () { needRedrawFlag = true; }
109 void refreshed () { needRedrawFlag = false; }
111 bool needRedraw () {
112 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
116 @property bool inMenu () { return (currPopup !is null); }
117 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; }
120 void setShip (int idx) {
121 if (eliteShipFiles.length) {
122 import core.memory : GC;
123 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
124 if (idx < 0) idx = 0;
125 if (shipModelIndex == idx && shipModel !is null) return;
126 // remove old ship
127 shipModelIndex = idx;
128 if (shipModel !is null) {
129 shipModel.glUnload();
130 shipModel.freeData();
131 shipModel.destroy;
132 shipModel = null;
134 GC.collect();
135 // load new ship
136 try {
137 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
138 shipModel.glUpload();
139 shipModel.freeImages();
140 } catch (Exception e) {}
141 //shipModel = eliteShips[shipModelIndex];
142 GC.collect();
146 void ensureShipModel () {
147 if (eliteShipFiles.length && shipModel is null) {
148 import std.random : uniform;
149 setShip(uniform!"[)"(0, eliteShipFiles.length));
153 void freeShipModel () {
154 if (!showShip && !inMenu && shipModel !is null) {
155 import core.memory : GC;
156 shipModel.glUnload();
157 shipModel.freeData();
158 shipModel.destroy;
159 shipModel = null;
160 GC.collect();
165 void loadState () {
166 try {
167 int widx = -1;
168 xiniParse(VFile(stateFileName),
169 "wordindex", &widx,
171 if (widx >= 0) goTo(widx);
172 } catch (Exception) {}
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;
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 conwriteln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
229 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();
243 void goTo (uint widx) {
244 if (laytext is null) return;
245 auto lidx = laytext.findLineWithWord(widx);
246 if (lidx != -1) {
247 assert(lidx < laytext.lineCount);
248 toMove = 0;
249 if (topY != laytext.line(lidx).y) {
250 topY = laytext.line(lidx).y;
251 stateChanged();
252 refresh();
254 version(none) {
255 conwriteln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
256 auto lnum = laytext.findLineAtY(topY);
257 conwriteln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
263 void pushPosition () {
264 if (laytext is null || laytext.lineCount == 0) return;
265 auto lidx = laytext.findLineAtY(topY);
266 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
269 void popPosition () {
270 if (posstack.length == 0) return;
271 auto widx = posstack[$-1];
272 posstack.length -= 1;
273 posstack.assumeSafeAppend;
274 goTo(widx);
278 void gotoSection (int sn) {
279 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
280 if (sn < 0 || sn >= bookmeta.sections.length) return;
281 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
282 if (lidx >= 0) {
283 auto newY = laytext.line(lidx).y;
284 if (newY != topY) {
285 topY = newY;
286 stateChanged();
287 refresh();
293 void relayout (bool forced=false) {
294 if (laytext !is null) {
295 uint widx;
296 auto lidx = laytext.findLineAtY(topY);
297 if (lidx >= 0) widx = laytext.line(lidx).wstart;
298 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
299 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
301 import core.time;
302 auto stt = MonoTime.currTime;
303 laytext.relayout(maxWidth, forced);
304 auto ett = MonoTime.currTime-stt;
305 conwriteln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
307 goTo(widx);
308 refresh();
313 void drawShipName () {
314 if (shipModel is null || shipModel.name.length == 0) return;
315 vg.fontFaceId(uiFont);
316 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
317 vg.fontSize(fsizeUI);
318 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
319 float h = BND_WIDGET_HEIGHT+8;
320 float mx = (GWidth-w)/2.0;
321 float my = (GHeight-h)-8;
322 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
323 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
324 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
325 my -= BND_WIDGET_HEIGHT+16;
326 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
327 mx = (GWidth-w)/2.0;
328 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
329 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
334 void createSectionMenu () {
335 closeMenu();
336 //conwriteln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
337 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
338 currPopup = new PopupMenu(vg, "Sections"d, () {
339 dstring[] items;
340 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
341 return items;
343 //conwriteln(currPopup.items.length);
344 // find current section
345 currPopup.curItemIdx = 0;
346 auto lidx = laytext.findLineAtY(topY);
347 if (lidx >= 0 && bookmeta.sections.length > 0) {
348 foreach (immutable sidx, const ref sc; bookmeta.sections) {
349 auto sline = laytext.findLineWithWord(sc.wordidx);
350 if (sline >= 0 && lidx >= sline) currPopup.curItemIdx = cast(int)sidx;
353 onPopupSelect = (int item) { gotoSection(item); };
357 void createQuitMenu (bool wantYes) {
358 closeMenu();
359 currPopup = new PopupMenu(vg, "Quit?"d, () {
360 return ["Yes"d, "No"d];
362 currPopup.curItemIdx = (wantYes ? 0 : 1);
363 onPopupSelect = (int item) { if (item == 0) concmd("quit"); };
367 void createRecentMenu () {
368 closeMenu();
369 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
370 if (recentFiles.length == 0) { freeShipModel(); return; }
371 currPopup = new PopupMenu(vg, "Recent files"d, () {
372 import std.conv : to;
373 dstring[] res;
374 foreach (const ref BookInfo bi; recentFiles) {
375 string s = bi.title;
376 if (bi.seqname.length) {
377 s ~= " (";
378 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
379 //conwriteln(bi.seqname);
380 s ~= bi.seqname;
381 s ~= ")";
383 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
384 res ~= s.to!dstring;
386 return res;
388 currPopup.curItemIdx = cast(int)recentFiles.length-1;
389 onPopupSelect = (int item) {
390 newBookFileName = recentFiles[item].diskfile;
391 popupNoShipKill = true;
396 bool menuKey (KeyEvent event) {
397 if (formatWorks != 0) return false;
398 if (!inMenu) return false;
399 if (inGalaxyMap) return false;
400 if (!event.pressed) return false;
401 auto res = currPopup.onKey(event);
402 if (res == PopupMenu.Close) {
403 closeMenu();
404 freeShipModel();
405 refresh();
406 } else if (res >= 0) {
407 if (onPopupSelect !is null) onPopupSelect(res);
408 closeMenu();
409 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
410 refresh();
412 return true;
416 bool menuMouse (MouseEvent event) {
417 if (formatWorks != 0) return false;
418 if (!inMenu) return false;
419 if (inGalaxyMap) return false;
420 auto res = currPopup.onMouse(event);
421 if (res == PopupMenu.Close) {
422 closeMenu();
423 freeShipModel();
424 refresh();
425 } else if (res >= 0) {
426 if (onPopupSelect !is null) onPopupSelect(res);
427 closeMenu();
428 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
429 refresh();
431 return true;
435 bool readerKey (KeyEvent event) {
436 if (formatWorks != 0) return false;
437 if (!event.pressed) {
438 switch (event.key) {
439 case Key.Up: arrowDir = 0; return true;
440 case Key.Down: arrowDir = 0; return true;
441 default:
443 return false;
445 switch (event.key) {
446 case Key.Space:
447 if (event.modifierState&ModifierState.shift) {
448 //goto case Key.PageUp;
449 hardScrollBy(toMove); toMove = 0;
450 scrollBy(-textHeight/3*2);
451 } else {
452 //goto case Key.PageDown;
453 hardScrollBy(toMove); toMove = 0;
454 scrollBy(textHeight/3*2);
456 break;
457 case Key.Backspace:
458 popPosition();
459 break;
460 case Key.PageUp:
461 hardScrollBy(toMove); toMove = 0;
462 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
463 break;
464 case Key.PageDown:
465 hardScrollBy(toMove); toMove = 0;
466 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
467 break;
468 case Key.Up:
469 //scrollBy(-8);
470 arrowDir = -1;
471 break;
472 case Key.Down:
473 //scrollBy(8);
474 arrowDir = 1;
475 break;
476 case Key.H:
477 goHome();
478 break;
479 default:
481 return true;
485 bool controlKey (KeyEvent event) {
486 if (!event.pressed) return false;
487 switch (event.key) {
488 case Key.Escape:
489 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
490 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
491 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
492 ensureShipModel();
493 createQuitMenu(true);
494 refresh();
495 return true;
496 case Key.P: if (event.modifierState == ModifierState.ctrl) { concmd("r_fps toggle"); return true; } break;
497 case Key.I: if (event.modifierState == ModifierState.ctrl) { concmd("r_interference toggle"); return true; } break;
498 case Key.N: if (event.modifierState == ModifierState.ctrl) { concmd("r_sbleft toggle"); return true; } break;
499 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
500 case Key.E:
501 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
502 if (!inMenu) {
503 showShip = !showShip;
504 if (showShip) ensureShipModel(); else freeShipModel();
505 refresh();
507 return true;
509 break;
510 case Key.Q: if (event.modifierState == ModifierState.ctrl) { concmd("quit"); return true; } break;
511 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
512 case Key.S:
513 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
514 ensureShipModel();
515 createSectionMenu();
516 refresh();
518 break;
519 case Key.L:
520 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
521 ensureShipModel();
522 createRecentMenu();
523 refresh();
525 break;
526 case Key.M:
527 if (!inMenu && !showShip) {
528 inGalaxyMap = !inGalaxyMap;
529 refresh();
531 break;
532 case Key.R:
533 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
534 break;
535 case Key.Home: if (showShip) { setShip(0); return true; } break;
536 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
537 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
538 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
539 default:
541 return showShip;
544 int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
545 int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
547 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
548 int endY () { pragma(inline, true); return startY+textHeight-1; }
551 // ////////////////////////////////////////////////////////////////////////// //
552 void run () {
553 if (GWidth < MinWinWidth) GWidth = MinWinWidth;
554 if (GHeight < MinWinHeight) GHeight = MinWinHeight;
556 bookText = loadBook(bookFileName);
558 //setOpenGLContextVersion(3, 2); // up to GLSL 150
559 setOpenGLContextVersion(2, 0); // it's enough
560 //openGLContextCompatible = false;
562 sdwindow = new SimpleWindow(GWidth, GHeight, bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast, OpenGlOptions.yes, Resizability.allowResizing);
563 sdwindow.hideCursor(); // we will do our own
564 sdwindow.setMinSize(MinWinWidth, MinWinHeight);
566 version(X11) sdwindow.closeQuery = delegate () { concmd("quit"); };
568 auto stt = MonoTime.currTime;
569 auto prevt = MonoTime.currTime;
570 auto curt = prevt;
571 textHeight = GHeight-8;
573 MonoTime nextIfTime = MonoTime.currTime;
575 lastMMove = MonoTime.currTime;
577 auto childTid = spawn(&reformatThreadFn, thisTid);
578 childTid.setMaxMailboxSize(128, OnCrowding.block);
579 thisTid.setMaxMailboxSize(128, OnCrowding.block);
581 void loadAndFormat (string filename) {
582 assert(formatWorks <= 0);
583 bookText = null;
584 laytext = null;
585 newYLine = -1;
586 //formatWorks = -1; //FIXME
587 firstFormat = true;
588 newYFade = false;
589 toMove = 0;
590 recentFiles = null;
591 arrowDir = 0;
592 arrowSpeed = 0;
593 //sdwindow.redrawOpenGlSceneNow();
594 //bookText = loadBook(newBookFileName);
595 //newBookFileName = null;
596 //reformat();
597 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
598 formatWorks = 1;
599 //conwriteln("*** loading new book: '", filename, "'");
600 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
601 refresh();
604 void reformat () {
605 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
606 childTid.send(ReformatWork(cast(shared)bookText, null, GWidth, GHeight));
607 refresh();
610 void formatComplete (ref ReformatWorkComplete w) {
611 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
613 auto lt = cast(LayText)w.laytext;
614 scope(exit) if (lt) lt.freeMemory();
615 w.laytext = null;
617 BookMetadata meta = cast(BookMetadata)w.meta;
618 w.meta = null;
620 BookText bt = cast(BookText)w.booktext;
621 w.booktext = null;
622 if (bt !is bookText) {
623 bookText = bt;
624 bookmeta = meta;
625 firstFormat = true;
626 sdwindow.title = bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast;
627 } else if (bookmeta is null) {
628 bookmeta = meta;
631 if (isQuitRequested || formatWorks <= 0) return;
632 --formatWorks;
634 if (w.w != GWidth) {
635 if (formatWorks == 0) reformat();
636 return;
639 if (formatWorks != 0) return;
640 freeShipModel();
642 uint widx = 0;
643 if (!firstFormat && laytext !is null && laytext.lineCount) {
644 auto lidx = laytext.findLineAtY(topY);
645 if (lidx >= 0) widx = laytext.line(lidx).wstart;
647 if (laytext !is null) laytext.freeMemory();
648 laytext = lt;
649 lt = null;
650 if (firstFormat) {
651 loadState();
652 firstFormat = false;
653 doSaveCheck = false;
654 stateChanged();
655 } else {
656 goTo(widx);
658 refresh();
661 void closeWindow () {
662 doSaveState(true); // forced state save
663 if (!sdwindow.closed && vg !is null) {
664 if (curImg >= 0) { vg.deleteImage(curImg); curImg = -1; }
665 if (curImgWhite >= 0) { vg.deleteImage(curImgWhite); curImgWhite = -1; }
666 freeShipModel();
667 vg.deleteGL2();
668 vg = null;
669 sdwindow.close();
673 sdwindow.visibleForTheFirstTime = delegate () {
674 sdwindow.setAsCurrentOpenGlContext(); // make this window active
675 sdwindow.vsync = false;
676 //sdwindow.useGLFinish = false;
677 //glbindLoadFunctions();
679 try {
680 uint flags = NVG_DEBUG;
681 if (flagNanoAA) flags |= NVG_ANTIALIAS;
682 if (flagNanoSS) flags |= NVG_STENCIL_STROKES;
683 if (!flagNanoFAA) flags |= !NVG_INVERT_FONT_AA;
684 vg = createGL2NVG(flags);
685 if (vg is null) {
686 conwriteln("Could not init nanovg.");
687 assert(0);
688 //sdwindow.close();
690 loadFonts(vg);
691 curImg = createCursorImage(vg);
692 curImgWhite = createCursorImage(vg, true);
693 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
694 } catch (Exception e) {
695 conwriteln("ERROR: ", e.msg);
696 concmd("quit");
697 return;
700 reformat();
702 refresh();
703 sdwindow.redrawOpenGlScene();
704 refresh();
707 sdwindow.windowResized = delegate (int w, int h) {
708 //conwriteln("w=", w, "; h=", h);
709 //if (w < MinWinWidth) w = MinWinWidth;
710 //if (h < MinWinHeight) h = MinWinHeight;
711 glViewport(0, 0, w, h);
712 GWidth = w;
713 GHeight = h;
714 textHeight = GHeight-8;
715 //reformat();
716 relayout();
717 refresh();
720 sdwindow.redrawOpenGlScene = delegate () {
721 if (isQuitRequested) return;
723 glconResize(GWidth/oglConScale, GHeight/oglConScale, oglConScale);
725 __gshared int cnt;
726 conwriteln("cnt=", cnt++);
729 //glClearColor(0, 0, 0, 0);
730 //glClearColor(0.18, 0.18, 0.18, 0);
731 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
732 glClear(glNVGClearFlags|EliteModel.glClearFlags);
734 refreshed();
735 needRedrawFlag = (fps !is null && fpsVisible);
736 if (vg is null) return;
738 scope(exit) vg.releaseImages();
739 vg.beginFrame(GWidth, GHeight, 1);
740 drawIfs(vg);
741 // draw scrollbar
743 float curHeight = (laytext !is null ? topY : 0);
744 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
745 if (th <= 0) { curHeight = 0; th = 1; }
746 float sz = cast(float)(GHeight-4)/th;
747 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
748 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
749 vg.bndScrollSlider(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
751 if (laytext is null) {
752 if (shipModel is null) {
753 vg.beginPath();
754 vg.fillColor(colorText);
755 int drawY = (GHeight-textHeight)/2;
756 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
757 vg.fontFaceId(textFont);
758 vg.fontSize(fsizeText);
759 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
760 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
761 vg.fill();
764 // draw text page
765 if (laytext !is null && laytext.lineCount) {
766 vg.beginPath();
767 vg.fillColor(colorText);
768 int drawY = startY;
769 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
770 //FIXME: not GHeight!
771 int lidx = laytext.findLineAtY(topY);
772 if (lidx >= 0 && lidx < laytext.lineCount) {
773 drawY -= topY-laytext.line(lidx).y;
774 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
775 LayFontStyle lastStyle;
776 int startx = startX;
777 while (lidx < laytext.lineCount && drawY < GHeight) {
778 auto ln = laytext.line(lidx);
779 foreach (ref LayWord w; laytext.lineWords(lidx)) {
780 if (lastStyle != w.style) {
781 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
782 vg.fontSize(w.style.fontsize);
783 auto c = NVGColor(w.style.color);
784 vg.fillColor(c);
785 //vg.strokeColor(c);
786 lastStyle = w.style;
788 // line highlighting
789 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
790 auto oid = w.objectIdx;
791 if (oid >= 0) {
792 vg.fill();
793 laytext.objects[oid].draw(vg, startx+w.x, drawY+ln.h+ln.desc);
794 vg.beginPath();
795 } else {
796 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
798 //TODO: draw lines over whitespace
799 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
800 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/2, w.w, 1);
801 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
802 version(debug_draw) {
803 vg.fill();
804 vg.beginPath();
805 vg.strokeWidth(1);
806 vg.strokeColor(nvgRGB(0, 0, 255));
807 vg.rect(startx+w.x, drawY, w.w, w.h);
808 vg.stroke();
809 vg.beginPath();
811 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
813 drawY += ln.h;
814 ++lidx;
817 vg.fill();
819 // dim text
820 if (!showShip) {
821 if (colorDim.a != 1) {
822 //vg.scissor(0, 0, GWidth, GHeight);
823 vg.resetScissor;
824 vg.beginPath();
825 vg.fillColor(colorDim);
826 vg.rect(0, 0, GWidth, GHeight);
827 vg.fill();
830 // dim more if menu is active
831 if (inMenu || showShip || formatWorks != 0) {
832 //vg.scissor(0, 0, GWidth, GHeight);
833 vg.resetScissor;
834 vg.beginPath();
835 //vg.globalAlpha(0.5);
836 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
837 vg.rect(0, 0, GWidth, GHeight);
838 vg.fill();
840 if (shipModel !is null) {
841 vg.endFrame();
842 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
843 zz += 10;
844 lightsClear();
846 lightAdd(
847 0, 60, 60,
848 1.0, 0.0, 0.0
851 lightAdd(
852 //0, 0, -60,
853 0, 0, -zz,
854 1.0, 1.0, 1.0
856 drawModel(shipAngle, shipModel);
857 vg.beginFrame(GWidth, GHeight, 1);
858 drawShipName();
859 //vg.endFrame();
861 if (formatWorks == 0) {
862 if (inMenu) {
863 //vg.beginFrame(GWidth, GHeight, 1);
864 //vg.scissor(0, 0, GWidth, GHeight);
865 vg.resetScissor;
866 currPopup.draw();
867 //vg.endFrame();
870 if (fps !is null && fpsVisible) {
871 //vg.beginFrame(GWidth, GHeight, 1);
872 //vg.scissor(0, 0, GWidth, GHeight);
873 vg.resetScissor;
874 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
875 //vg.endFrame();
877 if (inGalaxyMap) drawGalaxy(vg);
878 // mouse cursor
879 if (curImg >= 0 && !mouseHidden) {
880 int w, h;
881 //vg.beginFrame(GWidth, GHeight, 1);
882 vg.beginPath();
883 //vg.scissor(0, 0, GWidth, GHeight);
884 vg.resetScissor;
885 vg.imageSize(curImg, &w, &h);
886 if (mouseFadingAway) {
887 mouseAlpha -= 0.1;
888 if (mouseAlpha <= 0) { mouseFadingAway = false; mouseHidden = true; }
889 } else {
890 mouseAlpha = 1.0f;
892 vg.globalAlpha(mouseAlpha);
893 if (!mouseHigh) {
894 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
895 } else {
896 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
898 vg.rect(mouseX, mouseY, w, h);
899 vg.fill();
901 if (mouseHigh) {
902 vg.beginPath();
903 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
904 vg.rect(mouseX, mouseY, w, h);
905 vg.fill();
908 //vg.endFrame();
910 vg.endFrame();
911 glconDraw();
914 void processThreads () {
915 ReformatWorkComplete wd;
916 for (;;) {
917 bool workDone = false;
918 auto res = receiveTimeout(Duration.zero,
919 (QuitWork w) {
920 formatWorks = -1;
922 (ReformatWorkComplete w) {
923 wd = w;
924 workDone = true;
927 if (!res) { assert(!workDone); break; }
928 if (workDone) { workDone = false; formatComplete(wd); }
932 auto lastTimerEventTime = MonoTime.currTime;
933 bool somethingVisible = true;
935 sdwindow.visibilityChanged = delegate (bool vis) {
936 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
937 somethingVisible = vis;
940 conRegVar!fpsVisible("r_fps", "show fps indicator", (self, valstr) { refresh(); });
941 conRegVar!inGalaxyMap("r_galaxymap", "show Elite galaxy map", (self, valstr) { refresh(); });
943 conRegVar!interAllowed("r_interference", "show interference", (self, valstr) { refresh(); });
944 conRegVar!sbLeft("r_sbleft", "show scrollbar at the left side", (self, valstr) { refresh(); });
946 conRegVar!showShip("r_showship", "show Elite ship", (self, valstr) {
947 if (eliteShipFiles.length == 0) return false;
948 return true;
950 (self, valstr) {
951 if (showShip) ensureShipModel(); else freeShipModel();
952 refresh();
956 conRegFunc!(() {
957 if (currPopup !is null) {
958 currPopup.destroy;
959 currPopup = null;
960 freeShipModel();
961 refresh();
963 onPopupSelect = null;
964 })("menu_close", "close current popup menu");
966 conRegFunc!(() {
967 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
968 currPopup.destroy;
969 currPopup = null;
970 ensureShipModel();
971 createSectionMenu();
972 refresh();
974 })("menu_section", "show section menu");
976 conRegFunc!(() {
977 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
978 currPopup.destroy;
979 currPopup = null;
980 ensureShipModel();
981 createRecentMenu();
982 refresh();
984 })("menu_recent", "show recent menu");
986 sdwindow.eventLoop(1000/34,
987 delegate () {
988 processThreads();
989 if (sdwindow.closed) return;
990 conProcessQueue();
991 if (isQuitRequested) { closeWindow(); return; }
992 auto ctt = MonoTime.currTime;
995 auto spass = (ctt-lastTimerEventTime).total!"msecs";
996 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
997 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
998 lastTimerEventTime = ctt;
999 // update FPS timer
1000 prevt = curt;
1001 //curt = MonoTime.currTime;
1002 curt = ctt;
1003 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
1004 auto dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
1005 if (fps !is null) fps.update(dt);
1008 // smooth scrolling
1009 if (formatWorks == 0) {
1010 enum Delta = 92*2;
1011 if (toMove != 0) {
1012 import std.math : abs;
1013 immutable int sign = (toMove < 0 ? -1 : 1);
1014 // change speed
1015 if (arrowSpeed == 0) arrowSpeed = 16;
1016 if (abs(toMove) <= arrowSpeed) {
1017 arrowSpeed /= 2;
1018 if (arrowSpeed < 4) arrowSpeed = 4;
1019 } else {
1020 arrowSpeed *= 2;
1021 if (arrowSpeed > Delta) arrowSpeed = Delta;
1023 // calc move distance
1024 int sc = arrowSpeed;
1025 if (sc > abs(toMove)) sc = abs(toMove);
1026 hardScrollBy(sc*sign);
1027 toMove -= sc*sign;
1028 if (toMove == 0) arrowSpeed = 0;
1029 nextFadeTime = ctt+500.msecs;
1030 refresh();
1031 } else if (arrowDir) {
1032 if ((arrowDir < 0 && arrowSpeed > 0) || (arrowDir > 0 && arrowSpeed < 0)) arrowSpeed += arrowDir*4;
1033 arrowSpeed += arrowDir*2;
1034 if (arrowSpeed < -64) arrowSpeed = -64; else if (arrowSpeed > 64) arrowSpeed = 64;
1035 hardScrollBy(arrowSpeed);
1036 refresh();
1037 } else if (arrowSpeed != 0) {
1038 if (arrowSpeed < 0) {
1039 if ((arrowSpeed += 4) > 0) arrowSpeed = 0;
1040 } else {
1041 if ((arrowSpeed -= 4) < 0) arrowSpeed = 0;
1043 if (arrowSpeed) {
1044 hardScrollBy(arrowSpeed);
1045 refresh();
1048 // highlight fading
1049 if (newYFade) {
1050 if (ctt >= nextFadeTime) {
1051 if ((newYAlpha -= 0.1) <= 0) {
1052 newYFade = false;
1053 } else {
1054 nextFadeTime = ctt+25.msecs;
1056 refresh();
1060 // interference processing
1061 if (ctt >= nextIfTime) {
1062 import std.random : uniform;
1063 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
1064 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
1066 if (processIfs()) refresh();
1067 // ship rotation
1068 if (shipModel !is null) {
1069 shipAngle -= 1;
1070 if (shipAngle < 359) shipAngle += 360;
1071 refresh();
1073 // mouse autohide
1074 if (!mouseFadingAway) {
1075 if (!mouseHidden && !mouseHigh) {
1076 if ((ctt-lastMMove).total!"msecs" > 2500) {
1077 //mouseHidden = true;
1078 mouseFadingAway = true;
1079 mouseAlpha = 1.0f;
1080 refresh();
1084 if (somethingVisible) {
1085 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
1086 if (/*needRedraw*/true) sdwindow.redrawOpenGlSceneNow();
1087 } else {
1088 refresh();
1090 doSaveState();
1091 // load new book?
1092 if (newBookFileName.length && formatWorks == 0) {
1093 doSaveState(true); // forced state save
1094 closeMenu();
1095 ensureShipModel();
1096 loadAndFormat(newBookFileName);
1097 newBookFileName = null;
1098 sdwindow.redrawOpenGlSceneNow();
1099 //refresh();
1102 delegate (KeyEvent event) {
1103 if (sdwindow.closed) return;
1104 if (glconKeyEvent(event)) return;
1105 if (event.key == Key.PadEnter) event.key = Key.Enter;
1106 if ((event.modifierState&ModifierState.numLock) == 0) {
1107 switch (event.key) {
1108 case Key.Pad0: event.key = Key.Insert; break;
1109 case Key.PadDot: event.key = Key.Delete; break;
1110 case Key.Pad1: event.key = Key.End; break;
1111 case Key.Pad2: event.key = Key.Down; break;
1112 case Key.Pad3: event.key = Key.PageDown; break;
1113 case Key.Pad4: event.key = Key.Left; break;
1114 case Key.Pad6: event.key = Key.Right; break;
1115 case Key.Pad7: event.key = Key.Home; break;
1116 case Key.Pad8: event.key = Key.Up; break;
1117 case Key.Pad9: event.key = Key.PageUp; break;
1118 //case Key.PadEnter: event.key = Key.Enter; break;
1119 default:
1122 if (controlKey(event)) return;
1123 if (menuKey(event)) return;
1124 if (readerKey(event)) return;
1126 delegate (MouseEvent event) {
1127 if (sdwindow.closed) return;
1129 int linkAt (int msx, int msy) {
1130 if (laytext !is null && bookmeta !is null) {
1131 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
1132 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
1133 if (widx >= 0) {
1134 //conwriteln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1135 auto w = laytext.wordByIndex(widx);
1136 while (widx >= 0) {
1137 //conwriteln("word #", widx, "; href=", w.style.href);
1138 if (!w.style.href) break;
1139 if (auto hr = w.wordNum in bookmeta.hrefs) {
1140 dstring href = hr.name;
1141 if (href.length > 1 && href[0] == '#') {
1142 href = href[1..$];
1143 foreach (const ref id; bookmeta.ids) {
1144 if (id.name == href) {
1145 //pushPosition();
1146 //goTo(id.wordidx);
1147 return id.wordidx;
1150 //conwriteln("id '", hr.name, "' not found!");
1151 return -1;
1154 --widx;
1155 --w;
1160 return -1;
1163 lastMMove = MonoTime.currTime;
1164 if (mouseHidden || mouseFadingAway) {
1165 mouseHidden = false;
1166 mouseFadingAway = false;
1167 mouseAlpha = 1.0f;
1168 refresh();
1170 if (mouseX != event.x || mouseY != event.y) {
1171 mouseX = event.x;
1172 mouseY = event.y;
1173 refresh();
1175 if (!menuMouse(event) && !showShip) {
1176 if (event.type == MouseEventType.buttonPressed) {
1177 switch (event.button) {
1178 case MouseButton.wheelUp: hardScrollBy(-42); break;
1179 case MouseButton.wheelDown: hardScrollBy(42); break;
1180 case MouseButton.left:
1181 auto wid = linkAt(event.x, event.y);
1182 if (wid >= 0) {
1183 pushPosition();
1184 goTo(wid);
1186 break;
1187 case MouseButton.right:
1188 popPosition();
1189 break;
1190 default:
1194 mouseHigh = (linkAt(event.x, event.y) >= 0);
1196 delegate (dchar ch) {
1197 if (sdwindow.closed) return;
1198 if (glconCharEvent(ch)) return;
1199 //if (ch == '`') { concmd("r_console tan"); return; }
1202 closeWindow();
1204 childTid.send(QuitWork());
1205 while (formatWorks >= 0) processThreads();
1209 // ////////////////////////////////////////////////////////////////////////// //
1210 void main (string[] args) {
1211 import std.path;
1213 conRegVar!oglConScale(1, 4, "r_conscale", "console scale");
1215 universe = Galaxy(0);
1217 if (args.length == 1) {
1218 try {
1219 string lnn;
1220 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
1221 if (!line.isComment) lnn = line;
1223 if (lnn.length) args ~= lnn;
1224 } catch (Exception) {}
1226 if (args.length == 1) assert(0, "no filename");
1228 readConfig();
1230 bookFileName = args[1];
1231 run();