oops
[xreader.git] / xreader.d
blob303087423dcd312036a1bb68944288db2294ab4e
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 is aliced;
19 import core.time;
21 import std.concurrency;
22 import std.net.curl;
23 import std.regex;
25 import arsd.simpledisplay;
26 import arsd.image;
28 import iv.nanovega;
29 import iv.nanovega.textlayouter;
30 import iv.nanovega.blendish;
31 import iv.nanovega.perf;
33 import iv.sdbm;
34 import iv.strex;
36 import iv.cmdcongl;
38 import iv.unarray;
40 import iv.vfs;
41 import iv.vfs.io;
43 import xmodel;
44 import xiniz;
46 import booktext;
48 import xreadercfg;
49 import xreaderui;
50 import xreaderifs;
51 import xreaderfmt;
53 import eworld;
54 import ewbillboard;
56 //version = debug_draw;
59 // ////////////////////////////////////////////////////////////////////////// //
60 __gshared int oglConScale = 1;
63 // ////////////////////////////////////////////////////////////////////////// //
64 __gshared string bookFileName;
66 __gshared int formatWorks = -1;
67 __gshared NVGContext vg = null;
68 __gshared PerfGraph fps;
69 __gshared bool fpsVisible = false;
70 __gshared BookText bookText;
71 __gshared string newBookFileName;
72 __gshared uint[] posstack;
73 __gshared SimpleWindow sdwindow;
74 __gshared LayTextC laytext;
75 __gshared BookInfo[] recentFiles;
76 __gshared BookMetadata bookmeta;
78 __gshared int textHeight;
80 __gshared int toMove = 0; // for smooth scroller
81 __gshared int topY = 0;
82 __gshared int arrowDir = 0;
83 __gshared int lastArrowDir = 0;
84 __gshared float arrowSpeed = 0;
86 __gshared int newYLine = -1; // index
87 __gshared float newYAlpha;
88 __gshared bool newYFade = false;
89 __gshared MonoTime nextFadeTime;
91 __gshared NVGImage curImg, curImgWhite;
92 __gshared int mouseX = -666, mouseY = -666;
93 __gshared bool mouseHigh = false;
94 __gshared bool mouseHidden = false;
95 __gshared MonoTime lastMMove;
96 __gshared float mouseAlpha = 1.0f;
97 __gshared int mouseFadingAway = false;
99 __gshared bool needRedrawFlag = true;
101 __gshared bool firstFormat = true;
103 __gshared bool inGalaxyMap = false;
104 __gshared PopupMenu currPopup;
105 __gshared void delegate (int item) onPopupSelect;
106 __gshared bool delegate (KeyEvent event) onPopupKeyEvent; // return `true` to close menu
107 __gshared int shipModelIndex = 0;
108 __gshared bool popupNoShipKill = false;
110 __gshared bool doSaveCheck = false;
111 __gshared MonoTime nextSaveTime;
114 // ////////////////////////////////////////////////////////////////////////// //
115 struct HrefInfo {
116 int widx;
117 int x0, y0;
118 int width, height;
121 // all visible hrefs on a page
122 __gshared HrefInfo[] hreflist;
123 __gshared int hrefcurr = -1;
126 // ////////////////////////////////////////////////////////////////////////// //
127 struct Pulse {
128 public:
129 float PulseScale = 8; // ratio of "tail" to "acceleration"
131 private:
132 float PulseNormalize = 1;
134 private:
135 // viscous fluid with a pulse for part and decay for the rest
136 float pulseInternal (float x) nothrow @safe @nogc {
137 import std.math : exp;
138 float val;
140 // test
141 x = x*PulseScale;
142 if (x < 1) {
143 val = x-(1-exp(-x));
144 } else {
145 // the previous animation ended here:
146 float start = exp(-1.0f);
147 // simple viscous drag
148 x -= 1;
149 float expx = 1-exp(-x);
150 val = start+(expx*(1.0-start));
153 return val*PulseNormalize;
157 void ComputePulseScale () nothrow @safe @nogc {
158 PulseNormalize = 1.0f/pulseInternal(1);
161 public:
162 this (float ascale) nothrow @safe @nogc { PulseScale = ascale; }
164 void setScale (float ascale) nothrow @safe @nogc { PulseScale = ascale; PulseNormalize = 1; }
166 // viscous fluid with a pulse for part and decay for the rest
167 float pulse (float x) nothrow @safe @nogc {
168 if (x >= 1) return 1;
169 if (x <= 0) return 0;
171 if (PulseNormalize == 1) ComputePulseScale();
173 return pulseInternal(x);
178 __gshared Pulse pulse;
181 // ////////////////////////////////////////////////////////////////////////// //
182 void refresh () { needRedrawFlag = true; }
183 void refreshed () { needRedrawFlag = false; }
185 bool needRedraw () {
186 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
190 @property bool inMenu () { return (currPopup !is null); }
191 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; onPopupKeyEvent = null; }
194 void setShip (int idx) {
195 if (eliteShipFiles.length) {
196 import core.memory : GC;
197 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
198 if (idx < 0) idx = 0;
199 if (shipModelIndex == idx && shipModel !is null) return;
200 // remove old ship
201 shipModelIndex = idx;
202 if (shipModel !is null) {
203 shipModel.glUnload();
204 shipModel.freeData();
205 shipModel.destroy;
206 shipModel = null;
208 GC.collect();
209 // load new ship
210 try {
211 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
212 shipModel.glUpload();
213 shipModel.freeImages();
214 } catch (Exception e) {}
215 //shipModel = eliteShips[shipModelIndex];
216 GC.collect();
220 void ensureShipModel () {
221 if (eliteShipFiles.length && shipModel is null) {
222 import std.random : uniform;
223 setShip(uniform!"[)"(0, eliteShipFiles.length));
227 void freeShipModel () {
228 if (!showShip && !inMenu && shipModel !is null) {
229 import core.memory : GC;
230 shipModel.glUnload();
231 shipModel.freeData();
232 shipModel.destroy;
233 shipModel = null;
234 GC.collect();
239 void loadState () {
240 try {
241 int widx = -1;
242 xiniParse(VFile(stateFileName),
243 "wordindex", &widx,
245 if (widx >= 0) goTo(widx);
246 } catch (Exception) {}
249 void doSaveState (bool forced=false) {
250 if (!forced) {
251 if (formatWorks != 0) return;
252 if (!doSaveCheck) return;
253 auto ct = MonoTime.currTime;
254 if (ct < nextSaveTime) return;
255 } else {
256 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
258 try {
259 auto fo = VFile(stateFileName, "w");
260 if (laytext !is null && laytext.lineCount) {
261 auto lnum = laytext.findLineAtY(topY);
262 if (lnum >= 0) fo.writeln("wordindex=", laytext.line(lnum).wstart);
264 } catch (Exception) {}
265 doSaveCheck = false;
268 void stateChanged () {
269 if (!doSaveCheck) {
270 doSaveCheck = true;
271 nextSaveTime = MonoTime.currTime+10.seconds;
276 void hardScrollBy (int delta) {
277 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
278 int oldY = topY;
279 topY += delta;
280 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
281 if (topY < 0) topY = 0;
282 if (topY != oldY) {
283 stateChanged();
284 refresh();
288 void scrollBy (int delta) {
289 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
290 toMove += delta;
291 newYFade = true;
292 newYAlpha = 1;
293 if (delta < 0) {
294 // scrolling up, mark top line
295 newYLine = laytext.findLineAtY(topY);
296 } else {
297 // scrolling down, mark bottom line
298 newYLine = laytext.findLineAtY(topY+textHeight-2);
300 version(none) {
301 conwriteln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
303 if (newYLine < 0) newYFade = false;
307 void goHome () {
308 if (laytext is null) return;
309 if (topY != 0) {
310 topY = 0;
311 stateChanged();
312 refresh();
317 void goEnd () {
318 if (laytext is null || laytext.lineCount < 2) return;
319 auto newY = laytext.line(laytext.lineCount-1).y;
320 if (newY >= laytext.textHeight-textHeight) newY = cast(int)laytext.textHeight-textHeight;
321 if (newY < 0) newY = 0;
322 if (newY != topY) {
323 topY = newY;
324 stateChanged();
325 refresh();
330 void goTo (uint widx) {
331 if (laytext is null) return;
332 auto lidx = laytext.findLineWithWord(widx);
333 if (lidx != -1) {
334 assert(lidx < laytext.lineCount);
335 toMove = 0;
336 if (topY != laytext.line(lidx).y) {
337 topY = laytext.line(lidx).y;
338 stateChanged();
339 refresh();
341 version(none) {
342 conwriteln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
343 auto lnum = laytext.findLineAtY(topY);
344 conwriteln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
350 void pushPosition () {
351 if (laytext is null || laytext.lineCount == 0) return;
352 auto lidx = laytext.findLineAtY(topY);
353 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
356 void popPosition () {
357 if (posstack.length == 0) return;
358 auto widx = posstack[$-1];
359 posstack.length -= 1;
360 posstack.assumeSafeAppend;
361 goTo(widx);
365 void gotoSection (int sn) {
366 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
367 if (sn < 0 || sn >= bookmeta.sections.length) return;
368 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
369 if (lidx >= 0) {
370 auto newY = laytext.line(lidx).y;
371 if (newY != topY) {
372 topY = newY;
373 stateChanged();
374 refresh();
380 void relayout (bool forced=false) {
381 if (laytext !is null) {
382 uint widx;
383 auto lidx = laytext.findLineAtY(topY);
384 if (lidx >= 0) widx = laytext.line(lidx).wstart;
385 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
386 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
388 import core.time;
389 auto stt = MonoTime.currTime;
390 laytext.relayout(maxWidth, forced);
391 auto ett = MonoTime.currTime-stt;
392 conwriteln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
394 goTo(widx);
395 refresh();
400 void drawShipName () {
401 if (shipModel is null || shipModel.name.length == 0) return;
402 vg.fontFaceId(uiFont);
403 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
404 vg.fontSize(fsizeUI);
405 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
406 float h = BND_WIDGET_HEIGHT+8;
407 float mx = (GWidth-w)/2.0;
408 float my = (GHeight-h)-8;
409 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
410 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
411 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
412 my -= BND_WIDGET_HEIGHT+16;
413 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
414 mx = (GWidth-w)/2.0;
415 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
416 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
421 void createSectionMenu () {
422 closeMenu();
423 //conwriteln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
424 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
425 currPopup = new PopupMenu(vg, "Sections"d, () {
426 dstring[] items;
427 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
428 return items;
430 currPopup.allowFiltering = true;
431 //conwriteln(currPopup.items.length);
432 // find current section
433 currPopup.itemIndex = 0;
434 auto lidx = laytext.findLineAtY(topY);
435 if (lidx >= 0 && bookmeta.sections.length > 0) {
436 foreach (immutable sidx, const ref sc; bookmeta.sections) {
437 auto sline = laytext.findLineWithWord(sc.wordidx);
438 if (sline >= 0 && lidx >= sline) currPopup.itemIndex = cast(int)sidx;
441 onPopupSelect = (int item) { gotoSection(item); };
445 void createQuitMenu (bool wantYes) {
446 closeMenu();
447 currPopup = new PopupMenu(vg, "Quit?"d, () {
448 return ["Yes"d, "No"d];
450 currPopup.itemIndex = (wantYes ? 0 : 1);
451 onPopupSelect = (int item) { if (item == 0) concmd("quit"); };
455 void createRecentMenu () {
456 closeMenu();
457 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
458 if (recentFiles.length == 0) { freeShipModel(); return; }
459 currPopup = new PopupMenu(vg, "Recent files"d, () {
460 import std.conv : to;
461 dstring[] res;
462 foreach (const ref BookInfo bi; recentFiles) {
463 string s = bi.title;
464 if (bi.seqname.length) {
465 s ~= " (";
466 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
467 //conwriteln(bi.seqname);
468 s ~= bi.seqname;
469 s ~= ")";
471 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
472 res ~= s.to!dstring;
474 return res;
476 currPopup.allowFiltering = true;
477 currPopup.itemIndex = cast(int)recentFiles.length-1;
478 onPopupSelect = (int item) {
479 newBookFileName = recentFiles[item].diskfile;
480 popupNoShipKill = true;
482 onPopupKeyEvent = (KeyEvent event) {
483 if (event == "Delete" && currPopup.isCurValid) {
484 auto idx = currPopup.itemIndex;
485 if (idx >= 0 && idx < recentFiles.length) {
486 removeFileFromHistory(recentFiles[idx].diskfile);
487 currPopup.removeItem(idx);
488 foreach (immutable c; idx+1..recentFiles.length) recentFiles[c-1] = recentFiles[c];
489 recentFiles.length -= 1;
490 recentFiles.assumeSafeAppend;
493 return false; // don't close menu
498 bool menuKey (KeyEvent event) {
499 if (formatWorks != 0) return false;
500 if (!inMenu) return false;
501 if (inGalaxyMap) return false;
502 if (!event.pressed) return false;
503 if (currPopup is null) return false;
504 auto res = currPopup.onKey(event);
505 if (res == PopupMenu.Close) {
506 closeMenu();
507 freeShipModel();
508 refresh();
509 } else if (res >= 0) {
510 if (onPopupSelect !is null) onPopupSelect(res);
511 closeMenu();
512 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
513 refresh();
514 } else if (res == PopupMenu.NotMine) {
515 if (onPopupKeyEvent !is null && onPopupKeyEvent(event)) {
516 closeMenu();
517 freeShipModel();
518 refresh();
521 return true;
525 bool menuChar (dchar dch) {
526 if (formatWorks != 0) return false;
527 if (!inMenu) return false;
528 if (inGalaxyMap) return false;
529 if (currPopup is null) return false;
530 auto res = currPopup.onChar(dch);
531 if (res == PopupMenu.Close) {
532 closeMenu();
533 freeShipModel();
534 refresh();
535 } else if (res >= 0) {
536 if (onPopupSelect !is null) onPopupSelect(res);
537 closeMenu();
538 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
539 refresh();
541 return true;
545 bool menuMouse (MouseEvent event) {
546 if (formatWorks != 0) return false;
547 if (!inMenu) return false;
548 if (inGalaxyMap) return false;
549 if (currPopup is null) return false;
550 auto res = currPopup.onMouse(event);
551 if (res == PopupMenu.Close) {
552 closeMenu();
553 freeShipModel();
554 refresh();
555 } else if (res >= 0) {
556 if (onPopupSelect !is null) onPopupSelect(res);
557 closeMenu();
558 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
559 refresh();
561 return true;
565 int getCurrHrefIdx () {
566 if (hrefcurr < 0 || hrefcurr >= hreflist.length) return -1;
567 if (auto hr = hreflist[hrefcurr].widx in bookmeta.hrefs) {
568 dstring href = hr.name;
569 if (href.length > 1 && href[0] == '#') {
570 href = href[1..$];
571 foreach (const ref id; bookmeta.ids) if (id.name == href) return id.wordidx;
574 return -1;
578 bool readerKey (KeyEvent event) {
579 if (formatWorks != 0) return false;
580 if (!event.pressed) {
581 switch (event.key) {
582 case Key.Up: arrowDir = 0; return true;
583 case Key.Down: arrowDir = 0; return true;
584 default:
586 return false;
588 switch (event.key) {
589 case Key.Space:
590 if (event.modifierState&ModifierState.shift) {
591 //goto case Key.PageUp;
592 hardScrollBy(toMove); toMove = 0;
593 scrollBy(-textHeight/3*2);
594 } else {
595 //goto case Key.PageDown;
596 hardScrollBy(toMove); toMove = 0;
597 scrollBy(textHeight/3*2);
599 break;
600 case Key.Backspace:
601 popPosition();
602 break;
603 case Key.PageUp:
604 hardScrollBy(toMove); toMove = 0;
605 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
606 break;
607 case Key.PageDown:
608 hardScrollBy(toMove); toMove = 0;
609 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
610 break;
611 case Key.Up:
612 //scrollBy(-8);
613 arrowDir = -1;
614 break;
615 case Key.Down:
616 //scrollBy(8);
617 arrowDir = 1;
618 break;
619 case Key.H:
620 if (laytext !is null) {
621 if (event == "C-H") goEnd(); else goHome();
623 break;
624 case Key.Tab:
625 if (hreflist.length) {
626 int dir = (event == "Tab" ? 1 : event == "S-Tab" ? -1 : 0);
627 if (dir) {
628 if (hrefcurr < 0) {
629 hrefcurr = (dir > 0 ? 0 : cast(int)hreflist.length-1);
630 } else {
631 hrefcurr = (hrefcurr+cast(int)hreflist.length+dir)%cast(int)hreflist.length;
634 refresh();
636 break;
637 case Key.Enter:
638 if (hrefcurr >= 0 && event == "Enter") {
639 auto wid = getCurrHrefIdx();
640 if (wid >= 0) {
641 pushPosition();
642 goTo(wid);
645 break;
646 default:
648 return true;
652 bool controlKey (KeyEvent event) {
653 if (!event.pressed) return false;
654 switch (event.key) {
655 case Key.Escape:
656 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
657 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
658 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
659 if (hrefcurr >= 0) {
660 hrefcurr = -1;
661 } else {
662 ensureShipModel();
663 createQuitMenu(true);
665 refresh();
666 return true;
667 case Key.P: if (event.modifierState == ModifierState.ctrl) { concmd("r_fps toggle"); return true; } break;
668 case Key.I: if (event.modifierState == ModifierState.ctrl) { concmd("r_interference toggle"); return true; } break;
669 case Key.N: if (event.modifierState == ModifierState.ctrl) { concmd("r_sbleft toggle"); return true; } break;
670 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
671 case Key.E:
672 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
673 if (!inMenu) {
674 showShip = !showShip;
675 if (showShip) ensureShipModel(); else freeShipModel();
676 refresh();
678 return true;
680 break;
681 case Key.Q: if (event.modifierState == ModifierState.ctrl) { concmd("quit"); return true; } break;
682 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
683 case Key.S:
684 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
685 ensureShipModel();
686 createSectionMenu();
687 refresh();
689 break;
690 case Key.L:
691 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
692 ensureShipModel();
693 createRecentMenu();
694 refresh();
696 break;
697 case Key.M:
698 if (!inMenu && !showShip) {
699 inGalaxyMap = !inGalaxyMap;
700 refresh();
702 break;
703 case Key.R:
704 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
705 break;
706 case Key.Home: if (showShip) { setShip(0); return true; } break;
707 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
708 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
709 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
710 default:
712 return showShip;
715 int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
716 int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
718 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
719 int endY () { pragma(inline, true); return startY+textHeight-1; }
722 // ////////////////////////////////////////////////////////////////////////// //
723 void run () {
724 if (GWidth < MinWinWidth) GWidth = MinWinWidth;
725 if (GHeight < MinWinHeight) GHeight = MinWinHeight;
727 bookText = loadBook(bookFileName);
729 setOpenGLContextVersion(3, 0); // it's enough
730 //openGLContextCompatible = false;
732 sdwindow = new SimpleWindow(GWidth, GHeight, bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast, OpenGlOptions.yes, Resizability.allowResizing);
733 sdwindow.hideCursor(); // we will do our own
734 sdwindow.setMinSize(MinWinWidth, MinWinHeight);
736 version(X11) sdwindow.closeQuery = delegate () { concmd("quit"); };
738 auto stt = MonoTime.currTime;
739 auto prevt = MonoTime.currTime;
740 auto curt = prevt;
741 textHeight = GHeight-8;
743 MonoTime nextIfTime = MonoTime.currTime;
745 lastMMove = MonoTime.currTime;
747 auto childTid = spawn(&reformatThreadFn, thisTid);
748 childTid.setMaxMailboxSize(128, OnCrowding.block);
749 thisTid.setMaxMailboxSize(128, OnCrowding.block);
751 void loadAndFormat (string filename) {
752 assert(formatWorks <= 0);
753 bookText = null;
754 laytext = null;
755 newYLine = -1;
756 //formatWorks = -1; //FIXME
757 firstFormat = true;
758 newYFade = false;
759 toMove = 0;
760 recentFiles = null;
761 arrowDir = 0;
762 lastArrowDir = 0;
763 arrowSpeed = 0;
764 //sdwindow.redrawOpenGlSceneNow();
765 //bookText = loadBook(newBookFileName);
766 //newBookFileName = null;
767 //reformat();
768 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
769 formatWorks = 1;
770 //conwriteln("*** loading new book: '", filename, "'");
771 hreflist.unsafeArrayClear();
772 hrefcurr = -1;
773 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
774 refresh();
777 void reformat () {
778 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
779 childTid.send(ReformatWork(cast(shared)bookText, null, GWidth, GHeight));
780 refresh();
783 void formatComplete (ref ReformatWorkComplete w) {
784 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
786 auto lt = cast(LayTextC)w.laytext;
787 scope(exit) if (lt) lt.freeMemory();
788 w.laytext = null;
790 BookMetadata meta = cast(BookMetadata)w.meta;
791 w.meta = null;
793 BookText bt = cast(BookText)w.booktext;
794 w.booktext = null;
795 if (bt !is bookText) {
796 bookText = bt;
797 bookmeta = meta;
798 firstFormat = true;
799 sdwindow.title = bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast;
800 } else if (bookmeta is null) {
801 bookmeta = meta;
804 if (isQuitRequested || formatWorks <= 0) return;
805 --formatWorks;
807 if (w.w != GWidth) {
808 if (formatWorks == 0) reformat();
809 return;
812 if (formatWorks != 0) return;
813 freeShipModel();
815 uint widx = 0;
816 if (!firstFormat && laytext !is null && laytext.lineCount) {
817 auto lidx = laytext.findLineAtY(topY);
818 if (lidx >= 0) widx = laytext.line(lidx).wstart;
820 if (laytext !is null) laytext.freeMemory();
821 laytext = lt;
822 lt = null;
823 if (firstFormat) {
824 loadState();
825 firstFormat = false;
826 doSaveCheck = false;
827 stateChanged();
828 } else {
829 goTo(widx);
831 refresh();
834 void closeWindow () {
835 doSaveState(true); // forced state save
836 if (!sdwindow.closed && vg !is null) {
837 curImg.clear();
838 curImgWhite.clear();
839 freeShipModel();
840 vg.kill();
841 vg = null;
842 sdwindow.close();
846 sdwindow.visibleForTheFirstTime = delegate () {
847 sdwindow.setAsCurrentOpenGlContext(); // make this window active
848 sdwindow.vsync = false;
849 //sdwindow.useGLFinish = false;
850 //glbindLoadFunctions();
852 try {
853 NVGContextFlag[4] flagList;
854 uint flagCount = 0;
855 if (flagNanoAA) flagList[flagCount++] = NVGContextFlag.Antialias;
856 if (flagNanoSS) flagList[flagCount++] = NVGContextFlag.StencilStrokes;
857 if (flagNanoFAA) flagList[flagCount++] = NVGContextFlag.FontAA; else flagList[flagCount++] = NVGContextFlag.FontNoAA;
858 vg = nvgCreateContext(flagList[0..flagCount]);
859 if (vg is null) {
860 conwriteln("Could not init nanovg.");
861 assert(0);
862 //sdwindow.close();
864 loadFonts(vg);
865 curImg = createCursorImage(vg);
866 curImgWhite = createCursorImage(vg, true);
867 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
868 } catch (Exception e) {
869 conwriteln("ERROR: ", e.msg);
870 concmd("quit");
871 return;
874 reformat();
876 refresh();
877 sdwindow.redrawOpenGlScene();
878 refresh();
881 sdwindow.windowResized = delegate (int w, int h) {
882 //conwriteln("w=", w, "; h=", h);
883 //if (w < MinWinWidth) w = MinWinWidth;
884 //if (h < MinWinHeight) h = MinWinHeight;
885 glViewport(0, 0, w, h);
886 GWidth = w;
887 GHeight = h;
888 textHeight = GHeight-8;
889 //reformat();
890 relayout();
891 refresh();
894 static bool isWordHref (const ref LayWord w) {
895 if (!w.style.href) return false;
896 if (auto hr = w.wordNum in bookmeta.hrefs) {
897 dstring href = hr.name;
898 if (href.length > 1 && href[0] == '#') return true;
900 return false;
903 static bool isWordHrefByIdx (int widx) {
904 if (widx < 0) return false;
905 auto w = laytext.wordByIndex(widx);
906 if (!w) return false;
907 return isWordHref(*w);
910 sdwindow.redrawOpenGlScene = delegate () {
911 if (isQuitRequested) return;
913 glconResize(GWidth/oglConScale, GHeight/oglConScale, oglConScale);
915 __gshared int cnt;
916 conwriteln("cnt=", cnt++);
919 //glClearColor(0, 0, 0, 0);
920 //glClearColor(0.18, 0.18, 0.18, 0);
921 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
922 glClear(glNVGClearFlags|EliteModel.glClearFlags);
924 refreshed();
925 needRedrawFlag = (fps !is null && fpsVisible);
926 if (vg is null) return;
928 scope(exit) vg.releaseImages();
929 vg.beginFrame(GWidth, GHeight, 1);
930 drawIfs(vg);
931 // draw scrollbar
933 float curHeight = (laytext !is null ? topY : 0);
934 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
935 if (th <= 0) { curHeight = 0; th = 1; }
936 float sz = cast(float)(GHeight-4)/th;
937 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
938 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
939 vg.bndScrollSlider(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
942 int owidx = -1;
943 if (laytext is null) {
944 if (shipModel is null) {
945 vg.beginPath();
946 vg.fillColor(colorText);
947 int drawY = (GHeight-textHeight)/2;
948 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
949 vg.fontFaceId(textFont);
950 vg.fontSize(fsizeText);
951 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
952 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
953 vg.fill();
955 } else if (hrefcurr >= 0) {
956 owidx = hreflist[hrefcurr].widx;
959 // draw text page
960 int markerY = -666;
961 hreflist.unsafeArrayClear();
962 hrefcurr = -1;
963 if (laytext !is null && laytext.lineCount) {
964 vg.beginPath();
965 vg.fillColor(colorText);
966 int drawY = startY;
967 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
968 //FIXME: not GHeight!
969 int lidx = laytext.findLineAtY(topY);
970 if (lidx >= 0 && lidx < laytext.lineCount) {
971 drawY -= topY-laytext.line(lidx).y;
972 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
973 LayFontStyle lastStyle;
974 int startx = startX;
975 bool setColor = true;
976 while (lidx < laytext.lineCount && drawY < GHeight) {
977 auto ln = laytext.line(lidx);
978 foreach (ref LayWord w; laytext.lineWords(lidx)) {
979 if (lastStyle != w.style || setColor) {
980 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
981 vg.fontSize(w.style.fontsize);
982 auto c = NVGColor(w.style.color);
983 vg.fillColor(c);
984 //vg.strokeColor(c);
985 lastStyle = w.style;
986 setColor = false;
988 // line highlighting
989 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
990 auto oid = w.objectIdx;
991 if (oid >= 0) {
992 vg.fill();
993 laytext.objectAtIndex(oid).draw(vg, startx+w.x, drawY+ln.h+ln.desc);
994 vg.beginPath();
995 } else {
996 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
998 // register href
999 if (drawY >= 0 && drawY+ln.h <= GHeight && isWordHref(w)) {
1000 HrefInfo hif;
1001 hif.widx = cast(int)w.wordNum;
1002 hif.x0 = startx+w.x;
1003 hif.y0 = drawY;
1004 hif.width = w.w;
1005 hif.height = ln.h;
1006 hreflist.unsafeArrayAppend(hif);
1008 //TODO: draw lines over whitespace
1009 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
1010 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/3, w.w, 2);
1011 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
1012 version(debug_draw) {
1013 vg.fill();
1014 vg.beginPath();
1015 vg.strokeWidth(1);
1016 vg.strokeColor(nvgRGB(0, 0, 255));
1017 vg.rect(startx+w.x, drawY, w.w, w.h);
1018 vg.stroke();
1019 vg.beginPath();
1021 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
1023 if (newYFade && newYLine == lidx) markerY = drawY;
1024 drawY += ln.h;
1025 ++lidx;
1028 vg.fill();
1031 // restore current href (if it is possible)
1032 foreach (auto idx, const ref HrefInfo hif; hreflist) if (hif.widx == owidx) { hrefcurr = cast(int)idx; break; }
1034 // draw href
1035 if (hrefcurr >= 0) {
1036 // background
1037 vg.beginPath();
1038 vg.fillColor(nvgRGBA(0, 0, 0, 42));
1039 vg.rect(hreflist[hrefcurr].x0+0.5f, hreflist[hrefcurr].y0+0.5f, hreflist[hrefcurr].width, hreflist[hrefcurr].height);
1040 vg.fill();
1041 // frame
1042 vg.beginPath();
1043 vg.strokeWidth(1);
1044 vg.strokeColor(nvgRGB(0, 0, 255));
1045 vg.setLineDash([4.0f, 4.0f]);
1046 vg.rect(hreflist[hrefcurr].x0+0.5f, hreflist[hrefcurr].y0+0.5f, hreflist[hrefcurr].width, hreflist[hrefcurr].height);
1047 vg.stroke();
1048 vg.beginPath();
1051 // draw scroll marker
1052 if (markerY != -666) {
1053 vg.beginPath();
1054 vg.fillColor(NVGColor(0.3f, 0.3f, 0.3f, newYAlpha));
1055 vg.rect(startX, markerY, GWidth, 2);
1056 vg.fill();
1059 // dim text
1060 if (!showShip) {
1061 if (colorDim.a != 1) {
1062 //vg.scissor(0, 0, GWidth, GHeight);
1063 vg.resetScissor;
1064 vg.beginPath();
1065 vg.fillColor(colorDim);
1066 vg.rect(0, 0, GWidth, GHeight);
1067 vg.fill();
1071 // dim more if menu is active
1072 if (inMenu || showShip || formatWorks != 0) {
1073 //vg.scissor(0, 0, GWidth, GHeight);
1074 vg.resetScissor;
1075 vg.beginPath();
1076 //vg.globalAlpha(0.5);
1077 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
1078 vg.rect(0, 0, GWidth, GHeight);
1079 vg.fill();
1082 if (shipModel !is null) {
1083 vg.endFrame();
1084 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
1085 zz += 10;
1086 lightsClear();
1088 lightAdd(
1089 0, 60, 60,
1090 1.0, 0.0, 0.0
1093 lightAdd(
1094 //0, 0, -60,
1095 0, 0, -zz,
1096 1.0, 1.0, 1.0
1098 drawModel(shipAngle, shipModel);
1099 vg.beginFrame(GWidth, GHeight, 1);
1100 drawShipName();
1101 //vg.endFrame();
1104 if (formatWorks == 0) {
1105 if (inMenu) {
1106 //vg.beginFrame(GWidth, GHeight, 1);
1107 //vg.scissor(0, 0, GWidth, GHeight);
1108 vg.resetScissor;
1109 currPopup.draw();
1110 //vg.endFrame();
1114 if (fps !is null && fpsVisible) {
1115 //vg.beginFrame(GWidth, GHeight, 1);
1116 //vg.scissor(0, 0, GWidth, GHeight);
1117 vg.resetScissor;
1118 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
1119 //vg.endFrame();
1122 if (inGalaxyMap) drawGalaxy(vg);
1124 // mouse cursor
1125 if (curImg.valid && !mouseHidden) {
1126 int w, h;
1127 //vg.beginFrame(GWidth, GHeight, 1);
1128 vg.beginPath();
1129 //vg.scissor(0, 0, GWidth, GHeight);
1130 vg.resetScissor;
1131 vg.imageSize(curImg, w, h);
1132 if (mouseFadingAway) {
1133 mouseAlpha -= 0.1;
1134 if (mouseAlpha <= 0) { mouseFadingAway = false; mouseHidden = true; }
1135 } else {
1136 mouseAlpha = 1.0f;
1138 vg.globalAlpha(mouseAlpha);
1139 if (!mouseHigh) {
1140 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
1141 } else {
1142 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
1144 vg.rect(mouseX, mouseY, w, h);
1145 vg.fill();
1147 if (mouseHigh) {
1148 vg.beginPath();
1149 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
1150 vg.rect(mouseX, mouseY, w, h);
1151 vg.fill();
1154 //vg.endFrame();
1156 vg.endFrame();
1157 glconDraw();
1160 void processThreads () {
1161 ReformatWorkComplete wd;
1162 for (;;) {
1163 bool workDone = false;
1164 auto res = receiveTimeout(Duration.zero,
1165 (QuitWork w) {
1166 formatWorks = -1;
1168 (ReformatWorkComplete w) {
1169 wd = w;
1170 workDone = true;
1173 if (!res) { assert(!workDone); break; }
1174 if (workDone) { workDone = false; formatComplete(wd); }
1178 auto lastTimerEventTime = MonoTime.currTime;
1179 bool somethingVisible = true;
1181 sdwindow.visibilityChanged = delegate (bool vis) {
1182 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
1183 somethingVisible = vis;
1186 conRegVar!fpsVisible("r_fps", "show fps indicator", (self, valstr) { refresh(); });
1187 conRegVar!inGalaxyMap("r_galaxymap", "show Elite galaxy map", (self, valstr) { refresh(); });
1189 conRegVar!interAllowed("r_interference", "show interference", (self, valstr) { refresh(); });
1190 conRegVar!sbLeft("r_sbleft", "show scrollbar at the left side", (self, valstr) { refresh(); });
1192 conRegVar!showShip("r_showship", "show Elite ship", (self, valstr) {
1193 if (eliteShipFiles.length == 0) return false;
1194 return true;
1196 (self, valstr) {
1197 if (showShip) ensureShipModel(); else freeShipModel();
1198 refresh();
1202 conRegFunc!(() {
1203 if (currPopup !is null) {
1204 currPopup.destroy;
1205 currPopup = null;
1206 freeShipModel();
1207 refresh();
1209 onPopupSelect = null;
1210 })("menu_close", "close current popup menu");
1212 conRegFunc!(() {
1213 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
1214 currPopup.destroy;
1215 currPopup = null;
1216 ensureShipModel();
1217 createSectionMenu();
1218 refresh();
1220 })("menu_section", "show section menu");
1222 conRegFunc!(() {
1223 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
1224 currPopup.destroy;
1225 currPopup = null;
1226 ensureShipModel();
1227 createRecentMenu();
1228 refresh();
1230 })("menu_recent", "show recent menu");
1232 sdwindow.eventLoop(1000/34,
1233 delegate () {
1234 processThreads();
1235 if (sdwindow.closed) return;
1236 conProcessQueue();
1237 if (isQuitRequested) { closeWindow(); return; }
1238 auto ctt = MonoTime.currTime;
1241 auto spass = (ctt-lastTimerEventTime).total!"msecs";
1242 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
1243 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
1244 lastTimerEventTime = ctt;
1245 // update FPS timer
1246 prevt = curt;
1247 //curt = MonoTime.currTime;
1248 curt = ctt;
1249 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
1250 auto dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
1251 if (fps !is null) fps.update(dt);
1254 // smooth scrolling
1255 if (formatWorks == 0) {
1256 if (!lastArrowDir) lastArrowDir = arrowDir;
1257 if (arrowDir && arrowDir == lastArrowDir) {
1258 arrowSpeed += 0.015;
1259 if (arrowSpeed >= 1) arrowSpeed = 1;
1260 } else if (arrowDir) {
1261 assert(lastArrowDir != arrowDir);
1262 lastArrowDir = arrowDir;
1263 arrowSpeed *= 0.6;
1264 } else {
1265 arrowSpeed -= 0.025; //*(lastArrowDir ? 4 : 1);
1266 if (arrowSpeed <= 0) {
1267 arrowSpeed = 0;
1268 if (arrowDir) {
1269 lastArrowDir = arrowDir;
1273 if (toMove != 0) {
1274 import std.math : abs;
1275 enum Delta = 92*2;
1276 immutable int sign = (toMove < 0 ? -1 : 1);
1277 // change speed
1278 static int arrowSpeed = 0;
1279 if (arrowSpeed == 0) arrowSpeed = 16;
1280 if (abs(toMove) <= arrowSpeed) {
1281 arrowSpeed /= 2;
1282 if (arrowSpeed < 4) arrowSpeed = 4;
1283 } else {
1284 arrowSpeed *= 2;
1285 if (arrowSpeed > Delta) arrowSpeed = Delta;
1287 // calc move distance
1288 int sc = arrowSpeed;
1289 if (sc > abs(toMove)) sc = abs(toMove);
1290 hardScrollBy(sc*sign);
1291 toMove -= sc*sign;
1292 if (toMove == 0) arrowSpeed = 0;
1293 nextFadeTime = ctt+500.msecs;
1294 refresh();
1297 int toMove = cast(int)(pulse.pulse(arrowSpeed)*64);
1298 //if (arrowSpeed) { import std.stdio; writeln("lastdir=", lastArrowDir, "; dir=", arrowDir, "; as=", arrowSpeed, "; toMove=", toMove); }
1299 if (toMove > 1) {
1300 toMove *= lastArrowDir;
1301 hardScrollBy(toMove);
1302 nextFadeTime = ctt+500.msecs;
1303 refresh();
1307 enum Delta = 92*2;
1308 if (toMove != 0) {
1309 import std.math : abs;
1310 immutable int sign = (toMove < 0 ? -1 : 1);
1311 // change speed
1312 if (arrowSpeed == 0) arrowSpeed = 16;
1313 if (abs(toMove) <= arrowSpeed) {
1314 arrowSpeed /= 2;
1315 if (arrowSpeed < 4) arrowSpeed = 4;
1316 } else {
1317 arrowSpeed *= 2;
1318 if (arrowSpeed > Delta) arrowSpeed = Delta;
1320 // calc move distance
1321 int sc = arrowSpeed;
1322 if (sc > abs(toMove)) sc = abs(toMove);
1323 hardScrollBy(sc*sign);
1324 toMove -= sc*sign;
1325 if (toMove == 0) arrowSpeed = 0;
1326 nextFadeTime = ctt+500.msecs;
1327 refresh();
1328 } else if (arrowDir) {
1329 if ((arrowDir < 0 && arrowSpeed > 0) || (arrowDir > 0 && arrowSpeed < 0)) arrowSpeed += arrowDir*4;
1330 arrowSpeed += arrowDir*2;
1331 if (arrowSpeed < -64) arrowSpeed = -64; else if (arrowSpeed > 64) arrowSpeed = 64;
1332 hardScrollBy(arrowSpeed);
1333 refresh();
1334 } else if (arrowSpeed != 0) {
1335 if (arrowSpeed < 0) {
1336 if ((arrowSpeed += 4) > 0) arrowSpeed = 0;
1337 } else {
1338 if ((arrowSpeed -= 4) < 0) arrowSpeed = 0;
1340 if (arrowSpeed) {
1341 hardScrollBy(arrowSpeed);
1342 refresh();
1346 // highlight fading
1347 if (newYFade) {
1348 if (ctt >= nextFadeTime) {
1349 if ((newYAlpha -= 0.1) <= 0) {
1350 newYFade = false;
1351 } else {
1352 nextFadeTime = ctt+25.msecs;
1354 refresh();
1358 // interference processing
1359 if (ctt >= nextIfTime) {
1360 import std.random : uniform;
1361 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
1362 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
1364 if (processIfs()) refresh();
1365 // ship rotation
1366 if (shipModel !is null) {
1367 shipAngle -= 1;
1368 if (shipAngle < 359) shipAngle += 360;
1369 refresh();
1371 // mouse autohide
1372 if (!mouseFadingAway) {
1373 if (!mouseHidden && !mouseHigh) {
1374 if ((ctt-lastMMove).total!"msecs" > 2500) {
1375 //mouseHidden = true;
1376 mouseFadingAway = true;
1377 mouseAlpha = 1.0f;
1378 refresh();
1382 if (somethingVisible) {
1383 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
1384 if (/*needRedraw*/true) sdwindow.redrawOpenGlSceneNow();
1385 } else {
1386 refresh();
1388 doSaveState();
1389 // load new book?
1390 if (newBookFileName.length && formatWorks == 0 && vg !is null) {
1391 doSaveState(true); // forced state save
1392 closeMenu();
1393 ensureShipModel();
1394 loadAndFormat(newBookFileName);
1395 newBookFileName = null;
1396 sdwindow.redrawOpenGlSceneNow();
1397 //refresh();
1400 delegate (KeyEvent event) {
1401 if (sdwindow.closed) return;
1402 if (glconKeyEvent(event)) return;
1403 if (event.key == Key.PadEnter) event.key = Key.Enter;
1404 if ((event.modifierState&ModifierState.numLock) == 0) {
1405 switch (event.key) {
1406 case Key.Pad0: event.key = Key.Insert; break;
1407 case Key.PadDot: event.key = Key.Delete; break;
1408 case Key.Pad1: event.key = Key.End; break;
1409 case Key.Pad2: event.key = Key.Down; break;
1410 case Key.Pad3: event.key = Key.PageDown; break;
1411 case Key.Pad4: event.key = Key.Left; break;
1412 case Key.Pad6: event.key = Key.Right; break;
1413 case Key.Pad7: event.key = Key.Home; break;
1414 case Key.Pad8: event.key = Key.Up; break;
1415 case Key.Pad9: event.key = Key.PageUp; break;
1416 //case Key.PadEnter: event.key = Key.Enter; break;
1417 default:
1420 if (controlKey(event)) return;
1421 if (menuKey(event)) return;
1422 if (readerKey(event)) return;
1425 delegate (MouseEvent event) {
1426 if (sdwindow.closed) return;
1428 int linkAt (int msx, int msy) {
1429 if (laytext !is null && bookmeta !is null) {
1430 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
1431 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
1432 if (widx >= 0) {
1433 //conwriteln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1434 auto w = laytext.wordByIndex(widx);
1435 while (widx >= 0) {
1436 //conwriteln("word #", widx, "; href=", w.style.href);
1437 if (!w.style.href) break;
1438 if (auto hr = w.wordNum in bookmeta.hrefs) {
1439 dstring href = hr.name;
1440 if (href.length > 1 && href[0] == '#') {
1441 href = href[1..$];
1442 foreach (const ref id; bookmeta.ids) {
1443 if (id.name == href) {
1444 //pushPosition();
1445 //goTo(id.wordidx);
1446 return id.wordidx;
1449 //conwriteln("id '", hr.name, "' not found!");
1450 return -1;
1453 --widx;
1454 --w;
1459 return -1;
1462 lastMMove = MonoTime.currTime;
1463 if (mouseHidden || mouseFadingAway) {
1464 mouseHidden = false;
1465 mouseFadingAway = false;
1466 mouseAlpha = 1.0f;
1467 refresh();
1469 if (mouseX != event.x || mouseY != event.y) {
1470 mouseX = event.x;
1471 mouseY = event.y;
1472 refresh();
1474 if (!menuMouse(event) && !showShip) {
1475 if (event.type == MouseEventType.buttonPressed) {
1476 switch (event.button) {
1477 case MouseButton.wheelUp: hardScrollBy(-42); break;
1478 case MouseButton.wheelDown: hardScrollBy(42); break;
1479 case MouseButton.left:
1480 auto wid = linkAt(event.x, event.y);
1481 if (wid >= 0) {
1482 pushPosition();
1483 goTo(wid);
1485 break;
1486 case MouseButton.right:
1487 popPosition();
1488 break;
1489 default:
1493 mouseHigh = (linkAt(event.x, event.y) >= 0);
1495 delegate (dchar ch) {
1496 if (sdwindow.closed) return;
1497 if (glconCharEvent(ch)) return;
1498 if (menuChar(ch)) return;
1499 //if (ch == '`') { concmd("r_console tan"); return; }
1502 closeWindow();
1504 childTid.send(QuitWork());
1505 while (formatWorks >= 0) processThreads();
1509 // ////////////////////////////////////////////////////////////////////////// //
1510 struct FlibustaUrl {
1511 string fullUrl; // onion
1512 string host;
1513 string id;
1515 this (const(char)[] aurl) {
1516 import std.format : format;
1517 aurl = aurl.xstrip();
1518 auto flibustaRE = regex(`^(?:https?://)?(?:(?:(?:www\.)?flibusta\.[^/]+)|(?:flibustahezeous3\.onion))/b/(\d+)`);
1519 auto ct = aurl.matchFirst(flibustaRE);
1520 if (!ct.empty) {
1521 fullUrl = "http://flibustahezeous3.onion/b/%s/fb2".format(ct[1]);
1522 id = ct[1].idup;
1523 host = "flibustahezeous3.onion";
1524 } else {
1525 // add protocol
1526 auto protoRE = regex(`^([^:/]+):`);
1527 auto protoMt = aurl.matchFirst(protoRE);
1528 if (protoMt.empty) fullUrl = "http:%s%s".format((aurl[0] == '/' ? "" : "//"), aurl);
1529 // add host
1530 auto hostRE = regex(`^(?:[^:/]+)://([^/]+)`);
1531 auto hostMt = fullUrl.matchFirst(hostRE);
1532 if (hostMt.empty) { fullUrl = null; return; }
1533 host = hostMt[1].idup;
1537 @property bool valid () const pure nothrow @safe @nogc { pragma(inline, true); return (fullUrl.length > 0); }
1538 @property bool isFlibusta () const pure nothrow @safe @nogc { pragma(inline, true); return (id.length > 0); }
1539 @property bool isOnion () const pure nothrow @safe @nogc { pragma(inline, true); return host.endsWithCI(".onion"); }
1543 // ////////////////////////////////////////////////////////////////////////// //
1544 __gshared SDBM dbCache = null;
1547 void dbOpenCache () {
1548 if (dbCache is null) {
1549 import std.file : exists, mkdirRecurse;
1550 import std.path;
1551 string xfn = buildPath(RcDir, "cache");
1552 xfn.mkdirRecurse();
1553 xfn = buildPath(xfn, ".cache.db");
1554 dbCache = new SDBM(xfn, SDBM.WRITER|SDBM.CREAT|SDBM.NOLCK);
1559 char[] dbBuildNamePathById (char[] dest, const(char)[] id) {
1560 import core.stdc.stdio : snprintf;
1561 assert(id.length > 0);
1562 assert(dest.length > 0);
1563 auto len = snprintf(dest.ptr, dest.length, "/files/%.*s/name", cast(uint)id.length, id.ptr);
1564 if (len < 1 || len >= dest.length) assert(0, "out of destination buffer");
1565 return dest[0..len];
1569 string dbFindInCache() (in auto ref FlibustaUrl furl) {
1570 if (!furl.valid) return null;
1571 dbOpenCache();
1572 char[128] keybuf = void;
1573 auto dbpath = dbBuildNamePathById(keybuf[], furl.id);
1574 string fname = dbCache.get!string(dbpath);
1575 if (fname.length == 0) return null;
1576 import std.file : exists;
1577 try {
1578 if (fname.exists) return fname;
1579 } catch (Exception e) {}
1580 // no such file, remove it from database
1581 dbCache.del(dbpath);
1582 return null;
1586 void dbPutToCache() (in auto ref FlibustaUrl furl, const(char)[] fname) {
1587 if (!furl.valid || fname.length == 0) return;
1588 dbOpenCache();
1589 char[128] keybuf = void;
1590 dbCache.put(dbBuildNamePathById(keybuf[], furl.id), fname);
1594 void dbCloseCache () {
1595 if (dbCache is null) return;
1596 scope(exit) {
1597 delete dbCache;
1598 import core.memory : GC;
1599 GC.collect();
1600 GC.minimize();
1605 void dbCleanupCache () {
1606 dbOpenCache();
1607 bool[string] dead;
1608 auto xre = regex(`^/files/(.+)/name$`);
1609 dbCache.itInit();
1610 nextkey: for (;;) {
1611 auto key = dbCache.itNext();
1612 if (key is null) break;
1613 //writeln("[", key, "]");
1614 auto mt = key.matchFirst(xre);
1615 if (mt.empty) continue;
1616 auto fname = dbCache.get!string(key);
1617 if (fname.length != 0) {
1618 try {
1619 import std.file : exists;
1620 if (fname.exists) continue nextkey;
1621 } catch (Exception e) {}
1623 dead[key.idup] = true; // oops
1625 foreach (string k; dead.byKey) {
1626 //writeln("deleting stale cache record for '", k, "'");
1627 dbCache.del(k);
1629 dead.clear();
1633 // ////////////////////////////////////////////////////////////////////////// //
1634 // returns file name
1635 string fileDown() (in auto ref FlibustaUrl furl) {
1636 if (!furl.valid || !furl.isFlibusta) return null;
1638 string cachedFName = dbFindInCache(furl);
1639 if (cachedFName.length) return cachedFName;
1641 // content-disposition: attachment; filename="Divov_Sled-zombi.1lzb6Q.96382.fb2.zip"
1642 auto cdRE0 = regex(`^\s*attachment\s*;\s*filename="(.+?)"`, "i");
1643 auto cdRE1 = regex(`^\s*attachment\s*;\s*filename=([^;]+?)`, "i");
1645 auto http = HTTP(furl.host);
1646 http.method = HTTP.Method.get;
1647 http.url = furl.fullUrl;
1649 string fname = null;
1650 string tmpfname = null;
1651 string fnps = null;
1652 bool alreadyDowned = false;
1653 VFile fo;
1655 http.onReceiveHeader = delegate (in char[] key, in char[] value) {
1656 //writeln(key ~ ": " ~ value);
1657 if (key.strEquCI("content-disposition")) {
1658 auto ct = value.matchFirst(cdRE0);
1659 if (ct.empty) ct = value.matchFirst(cdRE1);
1660 if (ct[1].length) {
1661 auto fnp = ct[1].xstrip;
1662 auto lslpos = fnp.lastIndexOf('/');
1663 if (lslpos > 0) fnp = fnp[lslpos+1..$];
1664 if (fnp.length == 0) {
1665 fname = null;
1666 } else {
1667 import std.file : exists, mkdirRecurse;
1668 import std.path;
1669 string xfn = buildPath(RcDir, "cache");
1670 xfn.mkdirRecurse();
1671 char[] xxname;
1672 xxname.reserve(fnp.length);
1673 foreach (char ch; fnp) {
1674 if (ch <= ' ' || ch == 127) ch = '_';
1675 xxname ~= ch;
1677 fnps = cast(string)xxname; // it is safe to cast here
1678 fname = buildPath(xfn, fnps);
1679 tmpfname = fname~".down.part";
1681 if (fname.exists) {
1682 alreadyDowned = true;
1683 throw new Exception("already here");
1684 //throw new FileAlreadyDowned("already here");
1687 //write("\r", fnp, " [", furl.fullUrl, "]\e[K");
1693 http.onReceive = delegate (ubyte[] data) {
1694 if (!fo.isOpen) {
1695 if (fname.length == 0) throw new Exception("no file name found in headers");
1696 //writeln(" downloading to ", fname);
1697 fo = VFile(tmpfname, "w");
1699 fo.rawWriteExact(data);
1700 return data.length;
1703 MonoTime lastProgTime = MonoTime.zero;
1704 enum BarLength = 68;
1705 bool doProgUpdate = true;
1706 char[1024] buf = void;
1707 int oldDots = -1, oldPrc = -1;
1708 uint bufpos = 0;
1709 int stickPos = 1;
1710 immutable string stickStr = `|/-\`;
1712 // will set `doProgUpdate`, and update `oldXXX`
1713 void buildPBar (usize dlTotal, usize dlNow) {
1714 void put (const(char)[] s...) nothrow {
1715 if (s.length == 0) return;
1716 if (bufpos >= buf.length) return;
1717 int left = cast(int)buf.length-bufpos;
1718 if (s.length > left) s = s[0..left];
1719 assert(s.length > 0);
1720 import core.stdc.string : memcpy;
1721 memcpy(buf.ptr+bufpos, s.ptr, s.length);
1722 bufpos += cast(int)s.length;
1724 void putprc (int prc) {
1725 if (prc < 0) prc = 0; else if (prc > 100) prc = 100;
1726 if (bufpos >= buf.length || buf.length-bufpos < 5) return; // oops
1727 import core.stdc.stdio;
1728 bufpos += cast(int)snprintf(buf.ptr+bufpos, 5, "%3d%%", prc);
1730 void putCommaNum (usize n, usize max=0) {
1731 char[128] buf = void;
1732 if (max < n) {
1733 put(intWithCommas(buf[], n));
1734 } else {
1735 auto len = intWithCommas(buf[], max).length;
1736 auto pt = intWithCommas(buf[], n);
1737 while (len-- > pt.length) put(" ");
1738 put(pt);
1741 bufpos = 0;
1742 put("\r");
1743 put(fnps);
1744 put(" [");
1745 auto barpos = bufpos;
1746 foreach (immutable _; 0..BarLength) put(" ");
1747 put("]");
1748 if (dlTotal > 0) {
1749 int prc = cast(int)(cast(ulong)100*dlNow/dlTotal);
1750 if (prc < 0) prc = 0; else if (prc > 100) prc = 100;
1751 int dots = cast(int)(cast(ulong)BarLength*dlNow/dlTotal);
1752 if (dots < 0) dots = 0; else if (dots > BarLength) dots = BarLength;
1753 if (prc != oldPrc || dots != oldDots) {
1754 doProgUpdate = true;
1755 oldPrc = prc;
1756 oldDots = dots;
1758 put(" [");
1759 putCommaNum(dlNow, dlTotal);
1760 put("/");
1761 putCommaNum(dlTotal);
1762 put("] ");
1763 putprc(prc);
1764 // dots
1765 foreach (immutable dp; 0..dots) if (barpos+dp < buf.length) buf[barpos+dp] = '.';
1766 } else {
1767 put("?\e[K");
1768 if (oldDots != -1 || oldPrc != -1) doProgUpdate = true;
1769 oldDots = -1;
1770 oldPrc = -1;
1774 http.onProgress = delegate (usize dltotal, usize dlnow, usize ultotal, usize ulnow) {
1775 //writeln("Progress ", dltotal, ", ", dlnow, ", ", ultotal, ", ", ulnow);
1776 if (fname.length == 0) {
1777 auto ct = MonoTime.currTime;
1778 if ((ct-lastProgTime).total!"msecs" >= 100) {
1779 write("\x08", stickStr[stickPos]);
1780 stickPos = (stickPos+1)%cast(int)stickStr.length;
1781 lastProgTime = ct;
1783 return 0;
1785 buildPBar(dltotal, dlnow);
1786 if (doProgUpdate) { write("\e[?7l", buf[0..bufpos], "\e[K\e[?7h"); doProgUpdate = false; }
1787 //if (dltotal == 0) return 0;
1788 //auto ct = MonoTime.currTime;
1789 //if ((ct-lastProgTime).total!"msecs" < 1000) return 0;
1790 //lastProgTime = ct;
1791 //writef("\r%s [%s] -- [%s/%s] %3u%%\e[K", fnps, host, intWithCommas(dlnow), intWithCommas(dltotal), 100UL*dlnow/dltotal);
1792 return 0;
1795 if (furl.isOnion) {
1796 http.proxyType = HTTP.CurlProxy.socks5_hostname;
1797 http.proxy = "127.0.0.1";
1798 http.proxyPort = 9050;
1801 try {
1802 //write("downloading from [", host, "]: ", realUrl, " ... ");
1803 write("downloading from Flibusta: ", furl.fullUrl, " ... ", stickStr[0]);
1804 http.perform();
1805 if (fo.isOpen) {
1806 buildPBar(cast(uint)fo.size, cast(uint)fo.size);
1807 } else {
1808 buildPBar(1, 1);
1810 writeln(buf[0..bufpos], "\e[K");
1811 //write("\r\e[K");
1812 } catch (Exception e) {
1813 if (/*cast(FileAlreadyDowned)e*/alreadyDowned) {
1814 write("\r", fname, " already downloaded.\e[K");
1815 return fname; // already here
1817 if (tmpfname.length) {
1818 import std.exception : collectException;
1819 import std.file : remove;
1820 collectException(tmpfname.remove);
1822 throw e;
1825 if (fo.isOpen) {
1826 // something was downloaded, rename it
1827 import std.file : rename;
1828 fo.close();
1829 rename(tmpfname, fname);
1830 dbPutToCache(furl, fname);
1831 return fname;
1834 return null;
1838 // ////////////////////////////////////////////////////////////////////////// //
1839 void main (string[] args) {
1840 import std.path;
1842 conRegVar!oglConScale(1, 4, "r_conscale", "console scale");
1844 conProcessQueue(256*1024); // load config
1845 conProcessArgs!true(args);
1846 conProcessQueue(256*1024);
1848 universe = Galaxy(0);
1850 if (args.length == 1) {
1851 try {
1852 string lnn;
1853 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
1854 if (!line.isComment) lnn = line;
1856 if (lnn.length) args ~= lnn;
1857 } catch (Exception) {}
1858 } else {
1859 if (args.length != 2) assert(0, "invalid number of arguments");
1860 auto furl = FlibustaUrl(args[1]);
1861 if (furl.valid && furl.isFlibusta) {
1862 dbCleanupCache();
1863 scope(exit) dbCloseCache();
1864 string fn = fileDown(furl);
1865 if (fn.length == 0) assert(0, "can't download file");
1866 args[1] = fn;
1870 if (args.length == 1) assert(0, "no filename");
1872 readConfig();
1874 bookFileName = args[1];
1875 run();