nanovg API changes fix
[xreader.git] / xreader.d
blob58dbab65d74be8c0a3523d1cc1f0a55e7e0ab436
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.color;
25 import arsd.png;
26 import arsd.jpeg;
28 import iv.nanovg;
29 import iv.nanovg.oui.blendish;
30 import iv.nanovg.perf;
32 import iv.strex;
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 float dt = 0, secs = 0;
84 LayText laytext;
85 int textHeight = GHeight-8;
86 bool doSaveCheck = false;
87 MonoTime nextSaveTime;
89 MonoTime nextIfTime = MonoTime.currTime;
91 BookInfo[] recentFiles;
92 BookMetadata bookmeta;
94 int toMove = 0; // for smooth scroller
95 int topY = 0;
96 int arrowDir = 0;
98 int newYLine = -1; // index
99 float newYAlpha;
100 bool newYFade = false;
101 MonoTime nextFadeTime;
103 int curImg = -1, curImgWhite = -1;
104 int mouseX = -666, mouseY = -666;
105 bool mouseHigh = false;
106 bool mouseHidden = false;
107 auto lastMMove = MonoTime.currTime;
109 bool needRedrawFlag = true;
111 bool firstFormat = true;
113 bool inGalaxyMap = false;
114 PopupMenu currPopup;
115 void delegate (int item) onPopupSelect;
116 int shipModelIndex = 0;
117 bool popupNoShipKill = false;
119 void refresh () { needRedrawFlag = true; }
120 void refreshed () { needRedrawFlag = false; }
122 bool needRedraw () {
123 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
126 @property bool inMenu () { return (currPopup !is null); }
127 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; }
129 auto childTid = spawn(&reformatThreadFn, thisTid);
130 childTid.setMaxMailboxSize(128, OnCrowding.block);
131 thisTid.setMaxMailboxSize(128, OnCrowding.block);
133 void setShip (int idx) {
134 if (eliteShipFiles.length) {
135 import core.memory : GC;
136 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
137 if (idx < 0) idx = 0;
138 if (shipModelIndex == idx && shipModel !is null) return;
139 // remove old ship
140 shipModelIndex = idx;
141 if (shipModel !is null) {
142 shipModel.glUnload();
143 shipModel.freeData();
144 shipModel.destroy;
145 shipModel = null;
147 GC.collect();
148 // load new ship
149 try {
150 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
151 shipModel.glUpload();
152 shipModel.freeImages();
153 } catch (Exception e) {}
154 //shipModel = eliteShips[shipModelIndex];
155 GC.collect();
159 void ensureShipModel () {
160 if (eliteShipFiles.length && shipModel is null) {
161 import std.random : uniform;
162 setShip(uniform!"[)"(0, eliteShipFiles.length));
166 void freeShipModel () {
167 if (!showShip && !inMenu && shipModel !is null) {
168 import core.memory : GC;
169 shipModel.glUnload();
170 shipModel.freeData();
171 shipModel.destroy;
172 shipModel = null;
173 GC.collect();
177 void doSaveState (bool forced=false) {
178 if (!forced) {
179 if (formatWorks != 0) return;
180 if (!doSaveCheck) return;
181 auto ct = MonoTime.currTime;
182 if (ct < nextSaveTime) return;
183 } else {
184 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
186 try {
187 auto fo = VFile(stateFileName, "w");
188 if (laytext !is null && laytext.lineCount) {
189 auto lnum = laytext.findLineAtY(topY);
190 if (lnum >= 0) fo.writeln("wordindex=", laytext.line(lnum).wstart);
192 } catch (Exception) {}
193 doSaveCheck = false;
196 void stateChanged () {
197 if (!doSaveCheck) {
198 doSaveCheck = true;
199 nextSaveTime = MonoTime.currTime+10.seconds;
203 void hardScrollBy (int delta) {
204 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
205 int oldY = topY;
206 topY += delta;
207 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
208 if (topY < 0) topY = 0;
209 if (topY != oldY) {
210 stateChanged();
211 refresh();
215 void scrollBy (int delta) {
216 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
217 toMove += delta;
218 newYFade = true;
219 newYAlpha = 1;
220 if (delta < 0) {
221 // scrolling up, mark top line
222 newYLine = laytext.findLineAtY(topY);
223 } else {
224 // scrolling down, mark bottom line
225 newYLine = laytext.findLineAtY(topY+textHeight-2);
227 version(none) {
228 import std.stdio;
229 writeln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
231 if (newYLine < 0) newYFade = false;
234 void goHome () {
235 if (laytext is null) return;
236 if (topY != 0) {
237 topY = 0;
238 stateChanged();
239 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 import std.stdio;
256 writeln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
257 auto lnum = laytext.findLineAtY(topY);
258 writeln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
263 void loadState () {
264 try {
265 int widx = -1;
266 xiniParse(VFile(stateFileName),
267 "wordindex", &widx,
269 if (widx >= 0) goTo(widx);
270 } catch (Exception) {}
273 void loadAndFormat (string filename) {
274 assert(formatWorks <= 0);
275 booktext = null;
276 laytext = null;
277 newYLine = -1;
278 //formatWorks = -1; //FIXME
279 firstFormat = true;
280 newYFade = false;
281 toMove = 0;
282 recentFiles = null;
283 arrowDir = 0;
284 //sdwindow.redrawOpenGlSceneNow();
285 //booktext = loadBook(newBookFileName);
286 //newBookFileName = null;
287 //reformat();
288 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
289 formatWorks = 1;
290 //writeln("*** loading new book: '", filename, "'");
291 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
292 refresh();
295 void reformat () {
296 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
297 childTid.send(ReformatWork(cast(shared)booktext, null, GWidth, GHeight));
298 refresh();
301 void formatComplete (ref ReformatWorkComplete w) {
302 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
304 auto lt = cast(LayText)w.laytext;
305 scope(exit) if (lt) lt.freeMemory();
306 w.laytext = null;
308 BookMetadata meta = cast(BookMetadata)w.meta;
309 w.meta = null;
311 BookText bt = cast(BookText)w.booktext;
312 w.booktext = null;
313 if (bt !is booktext) {
314 booktext = bt;
315 bookmeta = meta;
316 firstFormat = true;
317 sdwindow.title = booktext.title~" \xe2\x80\x94 "~booktext.authorFirst~" "~booktext.authorLast;
318 } else if (bookmeta is null) {
319 bookmeta = meta;
322 if (doQuit || formatWorks <= 0) return;
323 --formatWorks;
325 if (w.w != GWidth) {
326 if (formatWorks == 0) reformat();
327 return;
330 if (formatWorks != 0) return;
331 freeShipModel();
333 uint widx = 0;
334 if (!firstFormat && laytext !is null && laytext.lineCount) {
335 auto lidx = laytext.findLineAtY(topY);
336 if (lidx >= 0) widx = laytext.line(lidx).wstart;
338 if (laytext !is null) laytext.freeMemory();
339 laytext = lt;
340 lt = null;
341 if (firstFormat) {
342 loadState();
343 firstFormat = false;
344 doSaveCheck = false;
345 stateChanged();
346 } else {
347 goTo(widx);
349 refresh();
352 void pushPosition () {
353 if (laytext is null || laytext.lineCount == 0) return;
354 auto lidx = laytext.findLineAtY(topY);
355 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
358 void popPosition () {
359 if (posstack.length == 0) return;
360 auto widx = posstack[$-1];
361 posstack.length -= 1;
362 posstack.assumeSafeAppend;
363 goTo(widx);
366 void gotoSection (int sn) {
367 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
368 if (sn < 0 || sn >= bookmeta.sections.length) return;
369 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
370 if (lidx >= 0) {
371 auto newY = laytext.line(lidx).y;
372 if (newY != topY) {
373 topY = newY;
374 stateChanged();
375 refresh();
380 version(X11) sdwindow.closeQuery = delegate () { doQuit = true; };
382 void closeWindow () {
383 doSaveState(true); // forced state save
384 if (!sdwindow.closed && vg !is null) {
385 if (curImg >= 0) { vg.deleteImage(curImg); curImg = -1; }
386 if (curImgWhite >= 0) { vg.deleteImage(curImgWhite); curImgWhite = -1; }
387 freeShipModel();
388 vg.deleteGL2();
389 vg = null;
390 sdwindow.close();
394 sdwindow.visibleForTheFirstTime = delegate () {
395 sdwindow.setAsCurrentOpenGlContext(); // make this window active
396 sdwindow.vsync = false;
397 //sdwindow.useGLFinish = false;
398 //glbindLoadFunctions();
400 try {
401 uint flags = NVG_DEBUG;
402 if (flagNanoAA) flags |= NVG_ANTIALIAS;
403 if (flagNanoSS) flags |= NVG_STENCIL_STROKES;
404 vg = createGL2NVG(flags);
405 if (vg is null) {
406 import std.stdio;
407 writeln("Could not init nanovg.");
408 assert(0);
409 //sdwindow.close();
411 loadFonts(vg);
412 curImg = createCursorImage(vg);
413 curImgWhite = createCursorImage(vg, true);
414 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
415 } catch (Exception e) {
416 import std.stdio : stderr;
417 stderr.writeln("ERROR: ", e.msg);
418 doQuit = true;
419 return;
422 reformat();
424 refresh();
425 sdwindow.redrawOpenGlScene();
426 refresh();
429 void relayout (bool forced=false) {
430 if (laytext !is null) {
431 uint widx;
432 auto lidx = laytext.findLineAtY(topY);
433 if (lidx >= 0) widx = laytext.line(lidx).wstart;
434 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
435 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
437 import core.time;
438 auto stt = MonoTime.currTime;
439 laytext.relayout(maxWidth, forced);
440 auto ett = MonoTime.currTime-stt;
441 writeln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
443 goTo(widx);
444 refresh();
448 sdwindow.windowResized = delegate (int w, int h) {
449 //writeln("w=", w, "; h=", h);
450 //if (w < MinWinWidth) w = MinWinWidth;
451 //if (h < MinWinHeight) h = MinWinHeight;
452 glViewport(0, 0, w, h);
453 GWidth = w;
454 GHeight = h;
455 textHeight = GHeight-8;
456 //reformat();
457 relayout();
460 void drawShipName () {
461 if (shipModel is null || shipModel.name.length == 0) return;
462 vg.fontFaceId(uiFont);
463 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
464 vg.fontSize(fsizeUI);
465 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
466 float h = BND_WIDGET_HEIGHT+8;
467 float mx = (GWidth-w)/2.0;
468 float my = (GHeight-h)-8;
469 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
470 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
471 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
472 my -= BND_WIDGET_HEIGHT+16;
473 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
474 mx = (GWidth-w)/2.0;
475 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
476 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
480 void createSectionMenu () {
481 closeMenu();
482 //writeln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
483 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
484 currPopup = new PopupMenu(vg, "Sections"d, () {
485 dstring[] items;
486 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
487 return items;
489 //writeln(currPopup.items.length);
490 // find current section
491 currPopup.curItemIdx = 0;
492 auto lidx = laytext.findLineAtY(topY);
493 if (lidx >= 0 && bookmeta.sections.length > 0) {
494 foreach (immutable sidx, const ref sc; bookmeta.sections) {
495 auto sline = laytext.findLineWithWord(sc.wordidx);
496 if (sline >= 0 && lidx >= sline) currPopup.curItemIdx = cast(int)sidx;
499 onPopupSelect = (int item) { gotoSection(item); };
502 void createQuitMenu (bool wantYes) {
503 closeMenu();
504 currPopup = new PopupMenu(vg, "Quit?"d, () {
505 return ["Yes"d, "No"d];
507 currPopup.curItemIdx = (wantYes ? 0 : 1);
508 onPopupSelect = (int item) { if (item == 0) doQuit = true; };
511 void createRecentMenu () {
512 closeMenu();
513 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
514 if (recentFiles.length == 0) { freeShipModel(); return; }
515 currPopup = new PopupMenu(vg, "Recent files"d, () {
516 import std.conv : to;
517 dstring[] res;
518 foreach (const ref BookInfo bi; recentFiles) {
519 string s = bi.title;
520 if (bi.seqname.length) {
521 s ~= " (";
522 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
523 //writeln(bi.seqname);
524 s ~= bi.seqname;
525 s ~= ")";
527 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
528 res ~= s.to!dstring;
530 return res;
532 currPopup.curItemIdx = cast(int)recentFiles.length-1;
533 onPopupSelect = (int item) {
534 newBookFileName = recentFiles[item].diskfile;
535 popupNoShipKill = true;
539 bool menuKey (KeyEvent event) {
540 if (formatWorks != 0) return false;
541 if (!inMenu) return false;
542 if (inGalaxyMap) return false;
543 if (!event.pressed) return false;
544 auto res = currPopup.onKey(event);
545 if (res == PopupMenu.Close) {
546 closeMenu();
547 freeShipModel();
548 refresh();
549 } else if (res >= 0) {
550 if (onPopupSelect !is null) onPopupSelect(res);
551 closeMenu();
552 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
553 refresh();
555 return true;
558 bool menuMouse (MouseEvent event) {
559 if (formatWorks != 0) return false;
560 if (!inMenu) return false;
561 if (inGalaxyMap) return false;
562 auto res = currPopup.onMouse(event);
563 if (res == PopupMenu.Close) {
564 closeMenu();
565 freeShipModel();
566 refresh();
567 } else if (res >= 0) {
568 if (onPopupSelect !is null) onPopupSelect(res);
569 closeMenu();
570 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
571 refresh();
573 return true;
576 bool readerKey (KeyEvent event) {
577 if (formatWorks != 0) return false;
578 if (!event.pressed) {
579 switch (event.key) {
580 case Key.Up: arrowDir = 0; return true;
581 case Key.Down: arrowDir = 0; return true;
582 default:
584 return false;
586 switch (event.key) {
587 case Key.Space:
588 if (event.modifierState&ModifierState.shift) {
589 //goto case Key.PageUp;
590 hardScrollBy(toMove); toMove = 0;
591 scrollBy(-textHeight/3*2);
592 } else {
593 //goto case Key.PageDown;
594 hardScrollBy(toMove); toMove = 0;
595 scrollBy(textHeight/3*2);
597 break;
598 case Key.Backspace:
599 popPosition();
600 break;
601 case Key.PageUp:
602 hardScrollBy(toMove); toMove = 0;
603 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
604 break;
605 case Key.PageDown:
606 hardScrollBy(toMove); toMove = 0;
607 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
608 break;
609 case Key.Up:
610 //scrollBy(-8);
611 arrowDir = -1;
612 break;
613 case Key.Down:
614 //scrollBy(8);
615 arrowDir = 1;
616 break;
617 case Key.H:
618 goHome();
619 break;
620 default:
622 return true;
625 bool controlKey (KeyEvent event) {
626 if (!event.pressed) return false;
627 switch (event.key) {
628 case Key.Escape:
629 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
630 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
631 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
632 ensureShipModel();
633 createQuitMenu(true);
634 refresh();
635 return true;
636 case Key.P: if (event.modifierState == ModifierState.ctrl) { fpsVisible = !fpsVisible; refresh(); return true; } break;
637 case Key.I: if (event.modifierState == ModifierState.ctrl) { interAllowed = !interAllowed; refresh(); return true; } break;
638 case Key.N: if (event.modifierState == ModifierState.ctrl) { sbLeft = !sbLeft; refresh(); return true; } break;
639 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
640 case Key.E:
641 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
642 if (!inMenu) {
643 showShip = !showShip;
644 if (showShip) ensureShipModel(); else freeShipModel();
645 refresh();
647 return true;
649 break;
650 case Key.Q: if (event.modifierState == ModifierState.ctrl) { doQuit = true; return true; } break;
651 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
652 case Key.S:
653 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
654 ensureShipModel();
655 createSectionMenu();
656 refresh();
658 break;
659 case Key.L:
660 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
661 ensureShipModel();
662 createRecentMenu();
663 refresh();
665 break;
666 case Key.M:
667 if (!inMenu && !showShip) {
668 inGalaxyMap = !inGalaxyMap;
669 refresh();
671 break;
672 case Key.R:
673 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
674 break;
675 case Key.Home: if (showShip) { setShip(0); return true; } break;
676 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
677 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
678 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
679 default:
681 return showShip;
684 static int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
685 static int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
687 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
688 int endY () { pragma(inline, true); return startY+textHeight-1; }
690 sdwindow.redrawOpenGlScene = delegate () {
691 if (doQuit) return;
692 // timers
693 prevt = curt;
694 curt = MonoTime.currTime;
695 secs = cast(double)((curt-stt).total!"msecs")/1000.0;
696 dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
698 //glClearColor(0, 0, 0, 0);
699 //glClearColor(0.18, 0.18, 0.18, 0);
700 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
701 glClear(glNVGClearFlags|EliteModel.glClearFlags);
703 refreshed();
704 needRedrawFlag = (fps !is null && fpsVisible);
705 if (fps !is null) fps.update(dt);
706 if (vg is null) return;
708 scope(exit) vg.releaseImages();
709 vg.beginFrame(GWidth, GHeight, 1);
710 drawIfs(vg);
711 // draw scrollbar
713 float curHeight = (laytext !is null ? topY : 0);
714 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
715 if (th <= 0) { curHeight = 0; th = 1; }
716 float sz = cast(float)(GHeight-4)/th;
717 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
718 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
719 vg.bndScrollBar(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
721 if (laytext is null) {
722 if (shipModel is null) {
723 vg.beginPath();
724 vg.fillColor(colorText);
725 int drawY = (GHeight-textHeight)/2;
726 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
727 vg.fontFaceId(textFont);
728 vg.fontSize(fsizeText);
729 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
730 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
731 vg.fill();
734 // draw text page
735 if (laytext !is null && laytext.lineCount) {
736 vg.beginPath();
737 vg.fillColor(colorText);
738 int drawY = startY;
739 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
740 //FIXME: not GHeight!
741 int lidx = laytext.findLineAtY(topY);
742 if (lidx >= 0 && lidx < laytext.lineCount) {
743 drawY -= topY-laytext.line(lidx).y;
744 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
745 LayFontStyle lastStyle;
746 int startx = startX;
747 while (lidx < laytext.lineCount && drawY < GHeight) {
748 auto ln = laytext.line(lidx);
749 foreach (ref LayWord w; laytext.lineWords(lidx)) {
750 if (lastStyle != w.style) {
751 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
752 vg.fontSize(w.style.fontsize);
753 auto c = NVGColor(w.style.color);
754 vg.fillColor(c);
755 //vg.strokeColor(c);
756 lastStyle = w.style;
758 // line highlighting
759 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
760 auto oid = w.objectIdx;
761 if (oid >= 0) {
762 vg.fill();
763 laytext.objects[oid].draw(vg, startx+w.x, drawY+ln.h+ln.desc);
764 vg.beginPath();
765 } else {
766 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
768 //TODO: draw lines over whitespace
769 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
770 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/2, w.w, 1);
771 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
772 version(debug_draw) {
773 vg.fill();
774 vg.beginPath();
775 vg.strokeWidth(1);
776 vg.strokeColor(nvgRGB(0, 0, 255));
777 vg.rect(startx+w.x, drawY, w.w, w.h);
778 vg.stroke();
779 vg.beginPath();
781 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
783 drawY += ln.h;
784 ++lidx;
787 vg.fill();
789 // dim text
790 if (!showShip) {
791 if (colorDim.a != 1) {
792 //vg.scissor(0, 0, GWidth, GHeight);
793 vg.resetScissor;
794 vg.beginPath();
795 vg.fillColor(colorDim);
796 vg.rect(0, 0, GWidth, GHeight);
797 vg.fill();
800 // dim more if menu is active
801 if (inMenu || showShip || formatWorks != 0) {
802 //vg.scissor(0, 0, GWidth, GHeight);
803 vg.resetScissor;
804 vg.beginPath();
805 //vg.globalAlpha(0.5);
806 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
807 vg.rect(0, 0, GWidth, GHeight);
808 vg.fill();
810 if (shipModel !is null) {
811 vg.endFrame();
812 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
813 zz += 10;
814 lightsClear();
816 lightAdd(
817 0, 60, 60,
818 1.0, 0.0, 0.0
821 lightAdd(
822 //0, 0, -60,
823 0, 0, -zz,
824 1.0, 1.0, 1.0
826 drawModel(shipAngle, shipModel);
827 vg.beginFrame(GWidth, GHeight, 1);
828 drawShipName();
829 //vg.endFrame();
831 if (formatWorks == 0) {
832 if (inMenu) {
833 //vg.beginFrame(GWidth, GHeight, 1);
834 //vg.scissor(0, 0, GWidth, GHeight);
835 vg.resetScissor;
836 currPopup.draw();
837 //vg.endFrame();
840 if (fps !is null && fpsVisible) {
841 //vg.beginFrame(GWidth, GHeight, 1);
842 //vg.scissor(0, 0, GWidth, GHeight);
843 vg.resetScissor;
844 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
845 //vg.endFrame();
847 if (inGalaxyMap) drawGalaxy(vg);
848 // mouse cursor
849 if (curImg >= 0 && !mouseHidden) {
850 int w, h;
851 //vg.beginFrame(GWidth, GHeight, 1);
852 vg.beginPath();
853 //vg.scissor(0, 0, GWidth, GHeight);
854 vg.resetScissor;
855 vg.imageSize(curImg, &w, &h);
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 sdwindow.eventLoop(1000/35,
895 delegate () {
896 processThreads();
897 if (sdwindow.closed) return;
898 if (doQuit) { closeWindow(); return; }
899 auto ctt = MonoTime.currTime;
900 // smooth scrolling
901 if (formatWorks == 0) {
902 enum Delta = 92*2;
903 if (toMove < 0) {
904 int sc = -toMove;
905 if (sc > Delta) sc = Delta;
906 hardScrollBy(-sc);
907 toMove += sc;
908 nextFadeTime = ctt+500.msecs;
909 } else if (toMove > 0) {
910 int sc = toMove;
911 if (sc > Delta) sc = Delta;
912 hardScrollBy(sc);
913 toMove -= sc;
914 nextFadeTime = ctt+500.msecs;
915 } else if (arrowDir) {
916 hardScrollBy(arrowDir*6*2);
918 // highlight fading
919 if (newYFade) {
920 if (ctt >= nextFadeTime) {
921 if ((newYAlpha -= 0.1) <= 0) {
922 newYFade = false;
923 } else {
924 nextFadeTime = ctt+25.msecs;
926 refresh();
930 // interference processing
931 if (ctt >= nextIfTime) {
932 import std.random : uniform;
933 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
934 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
936 if (processIfs()) refresh();
937 // ship rotation
938 if (shipModel !is null) {
939 shipAngle -= 1;
940 if (shipAngle < 359) shipAngle += 360;
941 refresh();
943 // mouse autohide
944 if (!mouseHidden && !mouseHigh) {
945 if ((ctt-lastMMove).total!"msecs" > 2500) {
946 mouseHidden = true;
947 refresh();
950 if (needRedraw) sdwindow.redrawOpenGlSceneNow();
951 doSaveState();
952 // load new book?
953 if (newBookFileName.length && formatWorks == 0) {
954 doSaveState(true); // forced state save
955 closeMenu();
956 ensureShipModel();
957 loadAndFormat(newBookFileName);
958 newBookFileName = null;
959 sdwindow.redrawOpenGlSceneNow();
960 //refresh();
963 delegate (KeyEvent event) {
964 if (sdwindow.closed) return;
965 if (event.key == Key.PadEnter) event.key = Key.Enter;
966 if ((event.modifierState&ModifierState.numLock) == 0) {
967 switch (event.key) {
968 case Key.Pad0: event.key = Key.Insert; break;
969 case Key.PadDot: event.key = Key.Delete; break;
970 case Key.Pad1: event.key = Key.End; break;
971 case Key.Pad2: event.key = Key.Down; break;
972 case Key.Pad3: event.key = Key.PageDown; break;
973 case Key.Pad4: event.key = Key.Left; break;
974 case Key.Pad6: event.key = Key.Right; break;
975 case Key.Pad7: event.key = Key.Home; break;
976 case Key.Pad8: event.key = Key.Up; break;
977 case Key.Pad9: event.key = Key.PageUp; break;
978 //case Key.PadEnter: event.key = Key.Enter; break;
979 default:
982 if (controlKey(event)) return;
983 if (menuKey(event)) return;
984 if (readerKey(event)) return;
986 delegate (MouseEvent event) {
987 int linkAt (int msx, int msy) {
988 if (laytext !is null && bookmeta !is null) {
989 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
990 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
991 if (widx >= 0) {
992 //writeln("word at (", msx-startX, ",", msy-startY, "): ", widx);
993 auto w = laytext.wordByIndex(widx);
994 while (widx >= 0) {
995 //writeln("word #", widx, "; href=", w.style.href);
996 if (!w.style.href) break;
997 if (auto hr = w.wordNum in bookmeta.hrefs) {
998 dstring href = hr.name;
999 if (href.length > 1 && href[0] == '#') {
1000 href = href[1..$];
1001 foreach (const ref id; bookmeta.ids) {
1002 if (id.name == href) {
1003 //pushPosition();
1004 //goTo(id.wordidx);
1005 return id.wordidx;
1008 //writeln("id '", hr.name, "' not found!");
1009 return -1;
1012 --widx;
1013 --w;
1018 return -1;
1021 lastMMove = MonoTime.currTime;
1022 if (mouseHidden) {
1023 mouseHidden = false;
1024 refresh();
1026 if (mouseX != event.x || mouseY != event.y) {
1027 mouseX = event.x;
1028 mouseY = event.y;
1029 refresh();
1031 if (!menuMouse(event) && !showShip) {
1032 if (event.type == MouseEventType.buttonPressed) {
1033 switch (event.button) {
1034 case MouseButton.wheelUp: hardScrollBy(-42); break;
1035 case MouseButton.wheelDown: hardScrollBy(42); break;
1036 case MouseButton.left:
1037 auto wid = linkAt(event.x, event.y);
1038 if (wid >= 0) {
1039 pushPosition();
1040 goTo(wid);
1042 break;
1043 case MouseButton.right:
1044 popPosition();
1045 break;
1046 default:
1050 mouseHigh = (linkAt(event.x, event.y) >= 0);
1052 delegate (dchar ch) {
1053 //if (ch == 'q') { doQuit = true; return; }
1056 closeWindow();
1058 childTid.send(QuitWork());
1059 while (formatWorks >= 0) processThreads();
1063 // ////////////////////////////////////////////////////////////////////////// //
1064 void main (string[] args) {
1065 import std.path;
1067 universe = Galaxy(0);
1069 if (args.length == 1) {
1070 try {
1071 string lnn;
1072 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
1073 if (!line.isComment) lnn = line;
1075 if (lnn.length) args ~= lnn;
1076 } catch (Exception) {}
1078 if (args.length == 1) assert(0, "no filename");
1080 readConfig();
1082 run(args[1]);