added command console (not really used yet)
[xreader.git] / xreader.d
blobddf5c195ca8770afc79ec042f31b951146968852
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.glcmdcon;
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 void run (string bookFileName) {
56 int formatWorks = -1;
57 NVGContext vg = null;
58 PerfGraph fps;
59 bool fpsVisible = false;
60 BookText booktext;
61 string newBookFileName;
63 if (GWidth < MinWinWidth) GWidth = MinWinWidth;
64 if (GHeight < MinWinHeight) GHeight = MinWinHeight;
66 uint[] posstack;
68 bool doQuit = false;
70 booktext = loadBook(bookFileName);
72 //setOpenGLContextVersion(3, 2); // up to GLSL 150
73 setOpenGLContextVersion(2, 0); // it's enough
74 //openGLContextCompatible = false;
76 auto sdwindow = new SimpleWindow(GWidth, GHeight, booktext.title~" \xe2\x80\x94 "~booktext.authorFirst~" "~booktext.authorLast, OpenGlOptions.yes, Resizablity.allowResizing);
77 sdwindow.hideCursor(); // we will do our own
78 sdwindow.setMinSize(MinWinWidth, MinWinHeight);
80 auto stt = MonoTime.currTime;
81 auto prevt = MonoTime.currTime;
82 auto curt = prevt;
83 LayText laytext;
84 int textHeight = GHeight-8;
85 bool doSaveCheck = false;
86 MonoTime nextSaveTime;
88 MonoTime nextIfTime = MonoTime.currTime;
90 BookInfo[] recentFiles;
91 BookMetadata bookmeta;
93 int toMove = 0; // for smooth scroller
94 int topY = 0;
95 int arrowDir = 0;
97 int newYLine = -1; // index
98 float newYAlpha;
99 bool newYFade = false;
100 MonoTime nextFadeTime;
102 int curImg = -1, curImgWhite = -1;
103 int mouseX = -666, mouseY = -666;
104 bool mouseHigh = false;
105 bool mouseHidden = false;
106 auto lastMMove = MonoTime.currTime;
107 float mouseAlpha = 1.0f;
108 int mouseFadingAway = false;
110 bool needRedrawFlag = true;
112 bool firstFormat = true;
114 bool inGalaxyMap = false;
115 PopupMenu currPopup;
116 void delegate (int item) onPopupSelect;
117 int shipModelIndex = 0;
118 bool popupNoShipKill = false;
120 void refresh () { needRedrawFlag = true; }
121 void refreshed () { needRedrawFlag = false; }
123 bool needRedraw () {
124 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
127 @property bool inMenu () { return (currPopup !is null); }
128 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; }
130 auto childTid = spawn(&reformatThreadFn, thisTid);
131 childTid.setMaxMailboxSize(128, OnCrowding.block);
132 thisTid.setMaxMailboxSize(128, OnCrowding.block);
134 void setShip (int idx) {
135 if (eliteShipFiles.length) {
136 import core.memory : GC;
137 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
138 if (idx < 0) idx = 0;
139 if (shipModelIndex == idx && shipModel !is null) return;
140 // remove old ship
141 shipModelIndex = idx;
142 if (shipModel !is null) {
143 shipModel.glUnload();
144 shipModel.freeData();
145 shipModel.destroy;
146 shipModel = null;
148 GC.collect();
149 // load new ship
150 try {
151 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
152 shipModel.glUpload();
153 shipModel.freeImages();
154 } catch (Exception e) {}
155 //shipModel = eliteShips[shipModelIndex];
156 GC.collect();
160 void ensureShipModel () {
161 if (eliteShipFiles.length && shipModel is null) {
162 import std.random : uniform;
163 setShip(uniform!"[)"(0, eliteShipFiles.length));
167 void freeShipModel () {
168 if (!showShip && !inMenu && shipModel !is null) {
169 import core.memory : GC;
170 shipModel.glUnload();
171 shipModel.freeData();
172 shipModel.destroy;
173 shipModel = null;
174 GC.collect();
178 void doSaveState (bool forced=false) {
179 if (!forced) {
180 if (formatWorks != 0) return;
181 if (!doSaveCheck) return;
182 auto ct = MonoTime.currTime;
183 if (ct < nextSaveTime) return;
184 } else {
185 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
187 try {
188 auto fo = VFile(stateFileName, "w");
189 if (laytext !is null && laytext.lineCount) {
190 auto lnum = laytext.findLineAtY(topY);
191 if (lnum >= 0) fo.writeln("wordindex=", laytext.line(lnum).wstart);
193 } catch (Exception) {}
194 doSaveCheck = false;
197 void stateChanged () {
198 if (!doSaveCheck) {
199 doSaveCheck = true;
200 nextSaveTime = MonoTime.currTime+10.seconds;
204 void hardScrollBy (int delta) {
205 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
206 int oldY = topY;
207 topY += delta;
208 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
209 if (topY < 0) topY = 0;
210 if (topY != oldY) {
211 stateChanged();
212 refresh();
216 void scrollBy (int delta) {
217 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
218 toMove += delta;
219 newYFade = true;
220 newYAlpha = 1;
221 if (delta < 0) {
222 // scrolling up, mark top line
223 newYLine = laytext.findLineAtY(topY);
224 } else {
225 // scrolling down, mark bottom line
226 newYLine = laytext.findLineAtY(topY+textHeight-2);
228 version(none) {
229 import std.stdio;
230 writeln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
232 if (newYLine < 0) newYFade = false;
235 void goHome () {
236 if (laytext is null) return;
237 if (topY != 0) {
238 topY = 0;
239 stateChanged();
240 refresh();
244 void goTo (uint widx) {
245 if (laytext is null) return;
246 auto lidx = laytext.findLineWithWord(widx);
247 if (lidx != -1) {
248 assert(lidx < laytext.lineCount);
249 toMove = 0;
250 if (topY != laytext.line(lidx).y) {
251 topY = laytext.line(lidx).y;
252 stateChanged();
253 refresh();
255 version(none) {
256 import std.stdio;
257 writeln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
258 auto lnum = laytext.findLineAtY(topY);
259 writeln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
264 void loadState () {
265 try {
266 int widx = -1;
267 xiniParse(VFile(stateFileName),
268 "wordindex", &widx,
270 if (widx >= 0) goTo(widx);
271 } catch (Exception) {}
274 void loadAndFormat (string filename) {
275 assert(formatWorks <= 0);
276 booktext = null;
277 laytext = null;
278 newYLine = -1;
279 //formatWorks = -1; //FIXME
280 firstFormat = true;
281 newYFade = false;
282 toMove = 0;
283 recentFiles = null;
284 arrowDir = 0;
285 //sdwindow.redrawOpenGlSceneNow();
286 //booktext = loadBook(newBookFileName);
287 //newBookFileName = null;
288 //reformat();
289 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
290 formatWorks = 1;
291 //writeln("*** loading new book: '", filename, "'");
292 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
293 refresh();
296 void reformat () {
297 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
298 childTid.send(ReformatWork(cast(shared)booktext, null, GWidth, GHeight));
299 refresh();
302 void formatComplete (ref ReformatWorkComplete w) {
303 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
305 auto lt = cast(LayText)w.laytext;
306 scope(exit) if (lt) lt.freeMemory();
307 w.laytext = null;
309 BookMetadata meta = cast(BookMetadata)w.meta;
310 w.meta = null;
312 BookText bt = cast(BookText)w.booktext;
313 w.booktext = null;
314 if (bt !is booktext) {
315 booktext = bt;
316 bookmeta = meta;
317 firstFormat = true;
318 sdwindow.title = booktext.title~" \xe2\x80\x94 "~booktext.authorFirst~" "~booktext.authorLast;
319 } else if (bookmeta is null) {
320 bookmeta = meta;
323 if (doQuit || formatWorks <= 0) return;
324 --formatWorks;
326 if (w.w != GWidth) {
327 if (formatWorks == 0) reformat();
328 return;
331 if (formatWorks != 0) return;
332 freeShipModel();
334 uint widx = 0;
335 if (!firstFormat && laytext !is null && laytext.lineCount) {
336 auto lidx = laytext.findLineAtY(topY);
337 if (lidx >= 0) widx = laytext.line(lidx).wstart;
339 if (laytext !is null) laytext.freeMemory();
340 laytext = lt;
341 lt = null;
342 if (firstFormat) {
343 loadState();
344 firstFormat = false;
345 doSaveCheck = false;
346 stateChanged();
347 } else {
348 goTo(widx);
350 refresh();
353 void pushPosition () {
354 if (laytext is null || laytext.lineCount == 0) return;
355 auto lidx = laytext.findLineAtY(topY);
356 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
359 void popPosition () {
360 if (posstack.length == 0) return;
361 auto widx = posstack[$-1];
362 posstack.length -= 1;
363 posstack.assumeSafeAppend;
364 goTo(widx);
367 void gotoSection (int sn) {
368 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
369 if (sn < 0 || sn >= bookmeta.sections.length) return;
370 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
371 if (lidx >= 0) {
372 auto newY = laytext.line(lidx).y;
373 if (newY != topY) {
374 topY = newY;
375 stateChanged();
376 refresh();
381 version(X11) sdwindow.closeQuery = delegate () { doQuit = true; };
383 void closeWindow () {
384 doSaveState(true); // forced state save
385 if (!sdwindow.closed && vg !is null) {
386 if (curImg >= 0) { vg.deleteImage(curImg); curImg = -1; }
387 if (curImgWhite >= 0) { vg.deleteImage(curImgWhite); curImgWhite = -1; }
388 freeShipModel();
389 vg.deleteGL2();
390 vg = null;
391 sdwindow.close();
395 sdwindow.visibleForTheFirstTime = delegate () {
396 sdwindow.setAsCurrentOpenGlContext(); // make this window active
397 sdwindow.vsync = false;
398 //sdwindow.useGLFinish = false;
399 //glbindLoadFunctions();
401 try {
402 uint flags = NVG_DEBUG;
403 if (flagNanoAA) flags |= NVG_ANTIALIAS;
404 if (flagNanoSS) flags |= NVG_STENCIL_STROKES;
405 vg = createGL2NVG(flags);
406 if (vg is null) {
407 import std.stdio;
408 writeln("Could not init nanovg.");
409 assert(0);
410 //sdwindow.close();
412 loadFonts(vg);
413 curImg = createCursorImage(vg);
414 curImgWhite = createCursorImage(vg, true);
415 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
416 } catch (Exception e) {
417 import std.stdio : stderr;
418 stderr.writeln("ERROR: ", e.msg);
419 doQuit = true;
420 return;
423 reformat();
425 refresh();
426 sdwindow.redrawOpenGlScene();
427 refresh();
430 void relayout (bool forced=false) {
431 if (laytext !is null) {
432 uint widx;
433 auto lidx = laytext.findLineAtY(topY);
434 if (lidx >= 0) widx = laytext.line(lidx).wstart;
435 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
436 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
438 import core.time;
439 auto stt = MonoTime.currTime;
440 laytext.relayout(maxWidth, forced);
441 auto ett = MonoTime.currTime-stt;
442 writeln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
444 goTo(widx);
445 refresh();
449 sdwindow.windowResized = delegate (int w, int h) {
450 //writeln("w=", w, "; h=", h);
451 //if (w < MinWinWidth) w = MinWinWidth;
452 //if (h < MinWinHeight) h = MinWinHeight;
453 glViewport(0, 0, w, h);
454 GWidth = w;
455 GHeight = h;
456 textHeight = GHeight-8;
457 //reformat();
458 relayout();
461 void drawShipName () {
462 if (shipModel is null || shipModel.name.length == 0) return;
463 vg.fontFaceId(uiFont);
464 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
465 vg.fontSize(fsizeUI);
466 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
467 float h = BND_WIDGET_HEIGHT+8;
468 float mx = (GWidth-w)/2.0;
469 float my = (GHeight-h)-8;
470 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
471 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
472 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
473 my -= BND_WIDGET_HEIGHT+16;
474 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
475 mx = (GWidth-w)/2.0;
476 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
477 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
481 void createSectionMenu () {
482 closeMenu();
483 //writeln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
484 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
485 currPopup = new PopupMenu(vg, "Sections"d, () {
486 dstring[] items;
487 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
488 return items;
490 //writeln(currPopup.items.length);
491 // find current section
492 currPopup.curItemIdx = 0;
493 auto lidx = laytext.findLineAtY(topY);
494 if (lidx >= 0 && bookmeta.sections.length > 0) {
495 foreach (immutable sidx, const ref sc; bookmeta.sections) {
496 auto sline = laytext.findLineWithWord(sc.wordidx);
497 if (sline >= 0 && lidx >= sline) currPopup.curItemIdx = cast(int)sidx;
500 onPopupSelect = (int item) { gotoSection(item); };
503 void createQuitMenu (bool wantYes) {
504 closeMenu();
505 currPopup = new PopupMenu(vg, "Quit?"d, () {
506 return ["Yes"d, "No"d];
508 currPopup.curItemIdx = (wantYes ? 0 : 1);
509 onPopupSelect = (int item) { if (item == 0) doQuit = true; };
512 void createRecentMenu () {
513 closeMenu();
514 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
515 if (recentFiles.length == 0) { freeShipModel(); return; }
516 currPopup = new PopupMenu(vg, "Recent files"d, () {
517 import std.conv : to;
518 dstring[] res;
519 foreach (const ref BookInfo bi; recentFiles) {
520 string s = bi.title;
521 if (bi.seqname.length) {
522 s ~= " (";
523 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
524 //writeln(bi.seqname);
525 s ~= bi.seqname;
526 s ~= ")";
528 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
529 res ~= s.to!dstring;
531 return res;
533 currPopup.curItemIdx = cast(int)recentFiles.length-1;
534 onPopupSelect = (int item) {
535 newBookFileName = recentFiles[item].diskfile;
536 popupNoShipKill = true;
540 bool menuKey (KeyEvent event) {
541 if (formatWorks != 0) return false;
542 if (!inMenu) return false;
543 if (inGalaxyMap) return false;
544 if (!event.pressed) return false;
545 auto res = currPopup.onKey(event);
546 if (res == PopupMenu.Close) {
547 closeMenu();
548 freeShipModel();
549 refresh();
550 } else if (res >= 0) {
551 if (onPopupSelect !is null) onPopupSelect(res);
552 closeMenu();
553 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
554 refresh();
556 return true;
559 bool menuMouse (MouseEvent event) {
560 if (formatWorks != 0) return false;
561 if (!inMenu) return false;
562 if (inGalaxyMap) return false;
563 auto res = currPopup.onMouse(event);
564 if (res == PopupMenu.Close) {
565 closeMenu();
566 freeShipModel();
567 refresh();
568 } else if (res >= 0) {
569 if (onPopupSelect !is null) onPopupSelect(res);
570 closeMenu();
571 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
572 refresh();
574 return true;
577 bool readerKey (KeyEvent event) {
578 if (formatWorks != 0) return false;
579 if (!event.pressed) {
580 switch (event.key) {
581 case Key.Up: arrowDir = 0; return true;
582 case Key.Down: arrowDir = 0; return true;
583 default:
585 return false;
587 switch (event.key) {
588 case Key.Space:
589 if (event.modifierState&ModifierState.shift) {
590 //goto case Key.PageUp;
591 hardScrollBy(toMove); toMove = 0;
592 scrollBy(-textHeight/3*2);
593 } else {
594 //goto case Key.PageDown;
595 hardScrollBy(toMove); toMove = 0;
596 scrollBy(textHeight/3*2);
598 break;
599 case Key.Backspace:
600 popPosition();
601 break;
602 case Key.PageUp:
603 hardScrollBy(toMove); toMove = 0;
604 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
605 break;
606 case Key.PageDown:
607 hardScrollBy(toMove); toMove = 0;
608 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
609 break;
610 case Key.Up:
611 //scrollBy(-8);
612 arrowDir = -1;
613 break;
614 case Key.Down:
615 //scrollBy(8);
616 arrowDir = 1;
617 break;
618 case Key.H:
619 goHome();
620 break;
621 default:
623 return true;
626 bool controlKey (KeyEvent event) {
627 if (!event.pressed) return false;
628 switch (event.key) {
629 case Key.Escape:
630 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
631 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
632 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
633 ensureShipModel();
634 createQuitMenu(true);
635 refresh();
636 return true;
637 case Key.P: if (event.modifierState == ModifierState.ctrl) { fpsVisible = !fpsVisible; refresh(); return true; } break;
638 case Key.I: if (event.modifierState == ModifierState.ctrl) { interAllowed = !interAllowed; refresh(); return true; } break;
639 case Key.N: if (event.modifierState == ModifierState.ctrl) { sbLeft = !sbLeft; refresh(); return true; } break;
640 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
641 case Key.E:
642 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
643 if (!inMenu) {
644 showShip = !showShip;
645 if (showShip) ensureShipModel(); else freeShipModel();
646 refresh();
648 return true;
650 break;
651 case Key.Q: if (event.modifierState == ModifierState.ctrl) { doQuit = true; return true; } break;
652 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
653 case Key.S:
654 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
655 ensureShipModel();
656 createSectionMenu();
657 refresh();
659 break;
660 case Key.L:
661 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
662 ensureShipModel();
663 createRecentMenu();
664 refresh();
666 break;
667 case Key.M:
668 if (!inMenu && !showShip) {
669 inGalaxyMap = !inGalaxyMap;
670 refresh();
672 break;
673 case Key.R:
674 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
675 break;
676 case Key.Home: if (showShip) { setShip(0); return true; } break;
677 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
678 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
679 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
680 default:
682 return showShip;
685 static int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
686 static int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
688 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
689 int endY () { pragma(inline, true); return startY+textHeight-1; }
691 sdwindow.redrawOpenGlScene = delegate () {
692 if (doQuit) return;
694 oglResizeConsole(GWidth, GHeight);
696 //glClearColor(0, 0, 0, 0);
697 //glClearColor(0.18, 0.18, 0.18, 0);
698 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
699 glClear(glNVGClearFlags|EliteModel.glClearFlags);
701 refreshed();
702 needRedrawFlag = (fps !is null && fpsVisible);
703 if (vg is null) return;
705 scope(exit) vg.releaseImages();
706 vg.beginFrame(GWidth, GHeight, 1);
707 drawIfs(vg);
708 // draw scrollbar
710 float curHeight = (laytext !is null ? topY : 0);
711 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
712 if (th <= 0) { curHeight = 0; th = 1; }
713 float sz = cast(float)(GHeight-4)/th;
714 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
715 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
716 vg.bndScrollSlider(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
718 if (laytext is null) {
719 if (shipModel is null) {
720 vg.beginPath();
721 vg.fillColor(colorText);
722 int drawY = (GHeight-textHeight)/2;
723 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
724 vg.fontFaceId(textFont);
725 vg.fontSize(fsizeText);
726 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
727 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
728 vg.fill();
731 // draw text page
732 if (laytext !is null && laytext.lineCount) {
733 vg.beginPath();
734 vg.fillColor(colorText);
735 int drawY = startY;
736 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
737 //FIXME: not GHeight!
738 int lidx = laytext.findLineAtY(topY);
739 if (lidx >= 0 && lidx < laytext.lineCount) {
740 drawY -= topY-laytext.line(lidx).y;
741 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
742 LayFontStyle lastStyle;
743 int startx = startX;
744 while (lidx < laytext.lineCount && drawY < GHeight) {
745 auto ln = laytext.line(lidx);
746 foreach (ref LayWord w; laytext.lineWords(lidx)) {
747 if (lastStyle != w.style) {
748 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
749 vg.fontSize(w.style.fontsize);
750 auto c = NVGColor(w.style.color);
751 vg.fillColor(c);
752 //vg.strokeColor(c);
753 lastStyle = w.style;
755 // line highlighting
756 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
757 auto oid = w.objectIdx;
758 if (oid >= 0) {
759 vg.fill();
760 laytext.objects[oid].draw(vg, startx+w.x, drawY+ln.h+ln.desc);
761 vg.beginPath();
762 } else {
763 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
765 //TODO: draw lines over whitespace
766 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
767 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/2, w.w, 1);
768 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
769 version(debug_draw) {
770 vg.fill();
771 vg.beginPath();
772 vg.strokeWidth(1);
773 vg.strokeColor(nvgRGB(0, 0, 255));
774 vg.rect(startx+w.x, drawY, w.w, w.h);
775 vg.stroke();
776 vg.beginPath();
778 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
780 drawY += ln.h;
781 ++lidx;
784 vg.fill();
786 // dim text
787 if (!showShip) {
788 if (colorDim.a != 1) {
789 //vg.scissor(0, 0, GWidth, GHeight);
790 vg.resetScissor;
791 vg.beginPath();
792 vg.fillColor(colorDim);
793 vg.rect(0, 0, GWidth, GHeight);
794 vg.fill();
797 // dim more if menu is active
798 if (inMenu || showShip || formatWorks != 0) {
799 //vg.scissor(0, 0, GWidth, GHeight);
800 vg.resetScissor;
801 vg.beginPath();
802 //vg.globalAlpha(0.5);
803 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
804 vg.rect(0, 0, GWidth, GHeight);
805 vg.fill();
807 if (shipModel !is null) {
808 vg.endFrame();
809 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
810 zz += 10;
811 lightsClear();
813 lightAdd(
814 0, 60, 60,
815 1.0, 0.0, 0.0
818 lightAdd(
819 //0, 0, -60,
820 0, 0, -zz,
821 1.0, 1.0, 1.0
823 drawModel(shipAngle, shipModel);
824 vg.beginFrame(GWidth, GHeight, 1);
825 drawShipName();
826 //vg.endFrame();
828 if (formatWorks == 0) {
829 if (inMenu) {
830 //vg.beginFrame(GWidth, GHeight, 1);
831 //vg.scissor(0, 0, GWidth, GHeight);
832 vg.resetScissor;
833 currPopup.draw();
834 //vg.endFrame();
837 if (fps !is null && fpsVisible) {
838 //vg.beginFrame(GWidth, GHeight, 1);
839 //vg.scissor(0, 0, GWidth, GHeight);
840 vg.resetScissor;
841 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
842 //vg.endFrame();
844 if (inGalaxyMap) drawGalaxy(vg);
845 // mouse cursor
846 if (curImg >= 0 && !mouseHidden) {
847 int w, h;
848 //vg.beginFrame(GWidth, GHeight, 1);
849 vg.beginPath();
850 //vg.scissor(0, 0, GWidth, GHeight);
851 vg.resetScissor;
852 vg.imageSize(curImg, &w, &h);
853 if (mouseFadingAway) {
854 mouseAlpha -= 0.1;
855 if (mouseAlpha <= 0) { mouseFadingAway = false; mouseHidden = true; }
856 } else {
857 mouseAlpha = 1.0f;
859 vg.globalAlpha(mouseAlpha);
860 if (!mouseHigh) {
861 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
862 } else {
863 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
865 vg.rect(mouseX, mouseY, w, h);
866 vg.fill();
868 if (mouseHigh) {
869 vg.beginPath();
870 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
871 vg.rect(mouseX, mouseY, w, h);
872 vg.fill();
875 //vg.endFrame();
877 vg.endFrame();
878 oglDrawConsole();
881 void processThreads () {
882 ReformatWorkComplete wd;
883 for (;;) {
884 bool workDone = false;
885 auto res = receiveTimeout(Duration.zero,
886 (QuitWork w) {
887 formatWorks = -1;
889 (ReformatWorkComplete w) {
890 wd = w;
891 workDone = true;
894 if (!res) { assert(!workDone); break; }
895 if (workDone) { workDone = false; formatComplete(wd); }
899 auto lastTimerEventTime = MonoTime.currTime;
900 bool somethingVisible = true;
902 sdwindow.visibilityChanged = delegate (bool vis) {
903 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
904 somethingVisible = vis;
907 sdwindow.eventLoop(1000/34,
908 delegate () {
909 processThreads();
910 if (sdwindow.closed) return;
911 concmdDoAll();
912 if (isQuitRequested || doQuit) { closeWindow(); return; }
913 auto ctt = MonoTime.currTime;
916 auto spass = (ctt-lastTimerEventTime).total!"msecs";
917 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
918 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
919 lastTimerEventTime = ctt;
920 // update FPS timer
921 prevt = curt;
922 //curt = MonoTime.currTime;
923 curt = ctt;
924 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
925 auto dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
926 if (fps !is null) fps.update(dt);
929 // smooth scrolling
930 if (formatWorks == 0) {
931 enum Delta = 92*2;
932 if (toMove < 0) {
933 int sc = -toMove;
934 if (sc > Delta) sc = Delta;
935 hardScrollBy(-sc);
936 toMove += sc;
937 nextFadeTime = ctt+500.msecs;
938 refresh();
939 } else if (toMove > 0) {
940 int sc = toMove;
941 if (sc > Delta) sc = Delta;
942 hardScrollBy(sc);
943 toMove -= sc;
944 nextFadeTime = ctt+500.msecs;
945 refresh();
946 } else if (arrowDir) {
947 hardScrollBy(arrowDir*6*2);
948 refresh();
950 // highlight fading
951 if (newYFade) {
952 if (ctt >= nextFadeTime) {
953 if ((newYAlpha -= 0.1) <= 0) {
954 newYFade = false;
955 } else {
956 nextFadeTime = ctt+25.msecs;
958 refresh();
962 // interference processing
963 if (ctt >= nextIfTime) {
964 import std.random : uniform;
965 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
966 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
968 if (processIfs()) refresh();
969 // ship rotation
970 if (shipModel !is null) {
971 shipAngle -= 1;
972 if (shipAngle < 359) shipAngle += 360;
973 refresh();
975 // mouse autohide
976 if (!mouseFadingAway) {
977 if (!mouseHidden && !mouseHigh) {
978 if ((ctt-lastMMove).total!"msecs" > 2500) {
979 //mouseHidden = true;
980 mouseFadingAway = true;
981 mouseAlpha = 1.0f;
982 refresh();
986 if (somethingVisible) {
987 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
988 if (/*needRedraw*/true) sdwindow.redrawOpenGlSceneNow();
989 } else {
990 refresh();
992 doSaveState();
993 // load new book?
994 if (newBookFileName.length && formatWorks == 0) {
995 doSaveState(true); // forced state save
996 closeMenu();
997 ensureShipModel();
998 loadAndFormat(newBookFileName);
999 newBookFileName = null;
1000 sdwindow.redrawOpenGlSceneNow();
1001 //refresh();
1004 delegate (KeyEvent event) {
1005 if (sdwindow.closed) return;
1006 if (conKeyEvent(event)) return;
1007 if (event.key == Key.PadEnter) event.key = Key.Enter;
1008 if ((event.modifierState&ModifierState.numLock) == 0) {
1009 switch (event.key) {
1010 case Key.Pad0: event.key = Key.Insert; break;
1011 case Key.PadDot: event.key = Key.Delete; break;
1012 case Key.Pad1: event.key = Key.End; break;
1013 case Key.Pad2: event.key = Key.Down; break;
1014 case Key.Pad3: event.key = Key.PageDown; break;
1015 case Key.Pad4: event.key = Key.Left; break;
1016 case Key.Pad6: event.key = Key.Right; break;
1017 case Key.Pad7: event.key = Key.Home; break;
1018 case Key.Pad8: event.key = Key.Up; break;
1019 case Key.Pad9: event.key = Key.PageUp; break;
1020 //case Key.PadEnter: event.key = Key.Enter; break;
1021 default:
1024 if (controlKey(event)) return;
1025 if (menuKey(event)) return;
1026 if (readerKey(event)) return;
1028 delegate (MouseEvent event) {
1029 if (sdwindow.closed) return;
1031 int linkAt (int msx, int msy) {
1032 if (laytext !is null && bookmeta !is null) {
1033 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
1034 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
1035 if (widx >= 0) {
1036 //writeln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1037 auto w = laytext.wordByIndex(widx);
1038 while (widx >= 0) {
1039 //writeln("word #", widx, "; href=", w.style.href);
1040 if (!w.style.href) break;
1041 if (auto hr = w.wordNum in bookmeta.hrefs) {
1042 dstring href = hr.name;
1043 if (href.length > 1 && href[0] == '#') {
1044 href = href[1..$];
1045 foreach (const ref id; bookmeta.ids) {
1046 if (id.name == href) {
1047 //pushPosition();
1048 //goTo(id.wordidx);
1049 return id.wordidx;
1052 //writeln("id '", hr.name, "' not found!");
1053 return -1;
1056 --widx;
1057 --w;
1062 return -1;
1065 lastMMove = MonoTime.currTime;
1066 if (mouseHidden || mouseFadingAway) {
1067 mouseHidden = false;
1068 mouseFadingAway = false;
1069 mouseAlpha = 1.0f;
1070 refresh();
1072 if (mouseX != event.x || mouseY != event.y) {
1073 mouseX = event.x;
1074 mouseY = event.y;
1075 refresh();
1077 if (!menuMouse(event) && !showShip) {
1078 if (event.type == MouseEventType.buttonPressed) {
1079 switch (event.button) {
1080 case MouseButton.wheelUp: hardScrollBy(-42); break;
1081 case MouseButton.wheelDown: hardScrollBy(42); break;
1082 case MouseButton.left:
1083 auto wid = linkAt(event.x, event.y);
1084 if (wid >= 0) {
1085 pushPosition();
1086 goTo(wid);
1088 break;
1089 case MouseButton.right:
1090 popPosition();
1091 break;
1092 default:
1096 mouseHigh = (linkAt(event.x, event.y) >= 0);
1098 delegate (dchar ch) {
1099 if (sdwindow.closed) return;
1100 //if (ch == 'q') { doQuit = true; return; }
1101 if (conCharEvent(ch)) return;
1102 if (ch == '`') { concmd("r_console tan"); return; }
1105 closeWindow();
1107 childTid.send(QuitWork());
1108 while (formatWorks >= 0) processThreads();
1112 // ////////////////////////////////////////////////////////////////////////// //
1113 void main (string[] args) {
1114 import std.path;
1116 initConsole();
1118 universe = Galaxy(0);
1120 if (args.length == 1) {
1121 try {
1122 string lnn;
1123 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
1124 if (!line.isComment) lnn = line;
1126 if (lnn.length) args ~= lnn;
1127 } catch (Exception) {}
1129 if (args.length == 1) assert(0, "no filename");
1131 readConfig();
1133 run(args[1]);