do not change page size
[xreader.git] / xreader.d
blob96269fe05c48f564cdf508e322874469fe3017bb
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.strex;
35 import iv.cmdcongl;
37 import iv.unarray;
39 import iv.vfs;
40 import iv.vfs.io;
42 import xmodel;
43 import xiniz;
45 import booktext;
47 import xreadercfg;
48 import xreaderui;
49 import xreaderifs;
50 import xreaderfmt;
52 import eworld;
53 import ewbillboard;
55 //version = debug_draw;
58 // ////////////////////////////////////////////////////////////////////////// //
59 __gshared int oglConScale = 1;
62 // ////////////////////////////////////////////////////////////////////////// //
63 __gshared string bookFileName;
65 __gshared int formatWorks = -1;
66 __gshared NVGContext vg = null;
67 __gshared PerfGraph fps;
68 __gshared bool fpsVisible = false;
69 __gshared BookText bookText;
70 __gshared string newBookFileName;
71 __gshared uint[] posstack;
72 __gshared SimpleWindow sdwindow;
73 __gshared LayTextC laytext;
74 __gshared BookInfo[] recentFiles;
75 __gshared BookMetadata bookmeta;
76 __gshared int currentSection = -666;
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 = getCurrentFileWordIndex();
243 xiniParse(VFile(stateFileName),
244 "wordindex", &widx,
247 if (widx >= 0) goTo(widx);
248 } catch (Exception) {}
251 void doSaveState (bool forced=false) {
252 if (!forced) {
253 if (formatWorks != 0) return;
254 if (!doSaveCheck) return;
255 auto ct = MonoTime.currTime;
256 if (ct < nextSaveTime) return;
257 } else {
258 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
260 try {
261 //auto fo = VFile(stateFileName, "w");
262 if (laytext !is null && laytext.lineCount) {
263 auto lnum = laytext.findLineAtY(topY);
264 if (lnum >= 0) {
265 //fo.writeln("wordindex=", laytext.line(lnum).wstart);
266 updateCurrentFileWordIndex(laytext.line(lnum).wstart);
269 } catch (Exception) {}
270 doSaveCheck = false;
273 void stateChanged () {
274 if (!doSaveCheck) {
275 doSaveCheck = true;
276 nextSaveTime = MonoTime.currTime+10.seconds;
281 void hardScrollBy (int delta) {
282 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
283 int oldY = topY;
284 topY += delta;
285 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
286 if (topY < 0) topY = 0;
287 if (topY != oldY) {
288 stateChanged();
289 refresh();
293 void scrollBy (int delta) {
294 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
295 toMove += delta;
296 newYFade = true;
297 newYAlpha = 1;
298 if (delta < 0) {
299 // scrolling up, mark top line
300 newYLine = laytext.findLineAtY(topY);
301 } else {
302 // scrolling down, mark bottom line
303 newYLine = laytext.findLineAtY(topY+textHeight-2);
305 version(none) {
306 conwriteln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
308 if (newYLine < 0) newYFade = false;
312 void goHome () {
313 if (laytext is null) return;
314 if (topY != 0) {
315 topY = 0;
316 stateChanged();
317 refresh();
322 void goEnd () {
323 if (laytext is null || laytext.lineCount < 2) return;
324 auto newY = laytext.line(laytext.lineCount-1).y;
325 if (newY >= laytext.textHeight-textHeight) newY = cast(int)laytext.textHeight-textHeight;
326 if (newY < 0) newY = 0;
327 if (newY != topY) {
328 topY = newY;
329 stateChanged();
330 refresh();
335 void goTo (uint widx) {
336 if (laytext is null) return;
337 auto lidx = laytext.findLineWithWord(widx);
338 if (lidx != -1) {
339 assert(lidx < laytext.lineCount);
340 toMove = 0;
341 if (topY != laytext.line(lidx).y) {
342 topY = laytext.line(lidx).y;
343 stateChanged();
344 refresh();
346 version(none) {
347 conwriteln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
348 auto lnum = laytext.findLineAtY(topY);
349 conwriteln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
355 void pushPosition () {
356 if (laytext is null || laytext.lineCount == 0) return;
357 auto lidx = laytext.findLineAtY(topY);
358 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
361 void popPosition () {
362 if (posstack.length == 0) return;
363 auto widx = posstack[$-1];
364 posstack.length -= 1;
365 posstack.assumeSafeAppend;
366 goTo(widx);
370 void gotoSection (int sn) {
371 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
372 if (sn < 0 || sn >= bookmeta.sections.length) return;
373 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
374 if (lidx >= 0) {
375 auto newY = laytext.line(lidx).y;
376 if (newY != topY) {
377 topY = newY;
378 stateChanged();
379 refresh();
385 void relayout (bool forced=false) {
386 if (laytext !is null) {
387 uint widx;
388 auto lidx = laytext.findLineAtY(topY);
389 if (lidx >= 0) widx = laytext.line(lidx).wstart;
390 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
391 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
393 import core.time;
394 auto stt = MonoTime.currTime;
395 laytext.relayout(maxWidth, forced);
396 auto ett = MonoTime.currTime-stt;
397 conwriteln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
399 goTo(widx);
400 refresh();
405 void drawShipName () {
406 if (shipModel is null || shipModel.name.length == 0) return;
407 vg.fontFaceId(uiFont);
408 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
409 vg.fontSize(fsizeUI);
410 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
411 float h = BND_WIDGET_HEIGHT+8;
412 float mx = (GWidth-w)/2.0;
413 float my = (GHeight-h)-8;
414 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
415 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
416 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
417 my -= BND_WIDGET_HEIGHT+16;
418 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
419 mx = (GWidth-w)/2.0;
420 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
421 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
426 void setWindowTitle (bool force=false) {
427 if (bookmeta is null || laytext is null) {
428 currentSection = -666;
429 return;
431 int scidx = -1;
432 if (bookmeta.sections.length > 1) {
433 auto lidx = laytext.findLineAtY(topY);
434 if (lidx >= 0) {
435 foreach (auto idx, ref cc; bookmeta.sections) {
436 if (idx == 0) continue;
437 auto sline = laytext.findLineWithWord(cc.wordidx);
438 if (sline >= 0 && lidx >= sline) scidx = cast(int)idx;
442 //Anc sc = null;
443 //if (scidx > 0 && scidx < bookmeta.sections.length) sc = bookmeta.sections[scidx];
444 if (force || scidx != currentSection) {
445 currentSection = scidx;
446 string tt;
447 if (scidx > 0 && scidx < bookmeta.sections.length) {
448 import std.conv : to;
449 tt = bookmeta.sections[scidx].name.to!string~" \xe2\x80\x94 ";
451 sdwindow.title = tt~bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast;
456 void createSectionMenu () {
457 closeMenu();
458 //conwriteln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
459 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
460 currPopup = new PopupMenu(vg, "Sections"d, () {
461 dstring[] items;
462 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
463 return items;
465 currPopup.allowFiltering = true;
466 //conwriteln(currPopup.items.length);
467 // find current section
468 currPopup.itemIndex = 0;
469 auto lidx = laytext.findLineAtY(topY);
470 if (lidx >= 0 && bookmeta.sections.length > 0) {
471 foreach (immutable sidx, const ref sc; bookmeta.sections) {
472 auto sline = laytext.findLineWithWord(sc.wordidx);
473 if (sline >= 0 && lidx >= sline) currPopup.itemIndex = cast(int)sidx;
476 onPopupSelect = (int item) { gotoSection(item); };
480 void createQuitMenu (bool wantYes) {
481 closeMenu();
482 currPopup = new PopupMenu(vg, "Quit?"d, () {
483 return ["Yes"d, "No"d];
485 currPopup.itemIndex = (wantYes ? 0 : 1);
486 onPopupSelect = (int item) { if (item == 0) concmd("quit"); };
490 void createRecentMenu () {
491 closeMenu();
492 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
493 if (recentFiles.length == 0) { freeShipModel(); return; }
494 currPopup = new PopupMenu(vg, "Recent files"d, () {
495 import std.conv : to;
496 dstring[] res;
497 foreach (const ref BookInfo bi; recentFiles) {
498 string s = bi.title;
499 if (bi.seqname.length) {
500 s ~= " (";
501 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
502 //conwriteln(bi.seqname);
503 s ~= bi.seqname;
504 s ~= ")";
506 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
507 res ~= s.to!dstring;
509 return res;
511 currPopup.allowFiltering = true;
512 currPopup.itemIndex = cast(int)recentFiles.length-1;
513 onPopupSelect = (int item) {
514 newBookFileName = recentFiles[item].diskfile;
515 popupNoShipKill = true;
517 onPopupKeyEvent = (KeyEvent event) {
518 if (event == "Delete" && currPopup.isCurValid) {
519 auto idx = currPopup.itemIndex;
520 if (idx >= 0 && idx < recentFiles.length) {
521 removeFileFromHistory(recentFiles[idx].diskfile);
522 currPopup.removeItem(idx);
523 foreach (immutable c; idx+1..recentFiles.length) recentFiles[c-1] = recentFiles[c];
524 recentFiles.length -= 1;
525 recentFiles.assumeSafeAppend;
528 return false; // don't close menu
533 bool menuKey (KeyEvent event) {
534 if (formatWorks != 0) return false;
535 if (!inMenu) return false;
536 if (inGalaxyMap) return false;
537 if (!event.pressed) return false;
538 if (currPopup is null) return false;
539 auto res = currPopup.onKey(event);
540 if (res == PopupMenu.Close) {
541 closeMenu();
542 freeShipModel();
543 refresh();
544 } else if (res >= 0) {
545 if (onPopupSelect !is null) onPopupSelect(res);
546 closeMenu();
547 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
548 refresh();
549 } else if (res == PopupMenu.NotMine) {
550 if (onPopupKeyEvent !is null && onPopupKeyEvent(event)) {
551 closeMenu();
552 freeShipModel();
553 refresh();
556 return true;
560 bool menuChar (dchar dch) {
561 if (formatWorks != 0) return false;
562 if (!inMenu) return false;
563 if (inGalaxyMap) return false;
564 if (currPopup is null) return false;
565 auto res = currPopup.onChar(dch);
566 if (res == PopupMenu.Close) {
567 closeMenu();
568 freeShipModel();
569 refresh();
570 } else if (res >= 0) {
571 if (onPopupSelect !is null) onPopupSelect(res);
572 closeMenu();
573 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
574 refresh();
576 return true;
580 bool menuMouse (MouseEvent event) {
581 if (formatWorks != 0) return false;
582 if (!inMenu) return false;
583 if (inGalaxyMap) return false;
584 if (currPopup is null) return false;
585 auto res = currPopup.onMouse(event);
586 if (res == PopupMenu.Close) {
587 closeMenu();
588 freeShipModel();
589 refresh();
590 } else if (res >= 0) {
591 if (onPopupSelect !is null) onPopupSelect(res);
592 closeMenu();
593 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
594 refresh();
596 return true;
600 int getCurrHrefIdx () {
601 if (hrefcurr < 0 || hrefcurr >= hreflist.length) return -1;
602 if (auto hr = hreflist[hrefcurr].widx in bookmeta.hrefs) {
603 dstring href = hr.name;
604 if (href.length > 1 && href[0] == '#') {
605 href = href[1..$];
606 foreach (const ref id; bookmeta.ids) if (id.name == href) return id.wordidx;
609 return -1;
613 bool readerKey (KeyEvent event) {
614 if (formatWorks != 0) return false;
615 if (!event.pressed) {
616 switch (event.key) {
617 case Key.Up: arrowDir = 0; return true;
618 case Key.Down: arrowDir = 0; return true;
619 default:
621 return false;
623 switch (event.key) {
624 case Key.Space:
625 if (event.modifierState&ModifierState.shift) {
626 //goto case Key.PageUp;
627 hardScrollBy(toMove); toMove = 0;
628 scrollBy(-textHeight/3*2);
629 } else {
630 //goto case Key.PageDown;
631 hardScrollBy(toMove); toMove = 0;
632 scrollBy(textHeight/3*2);
634 break;
635 case Key.Backspace:
636 popPosition();
637 break;
638 case Key.PageUp:
639 hardScrollBy(toMove); toMove = 0;
640 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
641 break;
642 case Key.PageDown:
643 hardScrollBy(toMove); toMove = 0;
644 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
645 break;
646 case Key.Up:
647 //scrollBy(-8);
648 arrowDir = -1;
649 break;
650 case Key.Down:
651 //scrollBy(8);
652 arrowDir = 1;
653 break;
654 case Key.H:
655 if (laytext !is null) {
656 if (event == "C-H") goEnd(); else goHome();
658 break;
659 case Key.Tab:
660 if (hreflist.length) {
661 int dir = (event == "Tab" ? 1 : event == "S-Tab" ? -1 : 0);
662 if (dir) {
663 if (hrefcurr < 0) {
664 hrefcurr = (dir > 0 ? 0 : cast(int)hreflist.length-1);
665 } else {
666 hrefcurr = (hrefcurr+cast(int)hreflist.length+dir)%cast(int)hreflist.length;
669 refresh();
671 break;
672 case Key.Enter:
673 if (hrefcurr >= 0 && event == "Enter") {
674 auto wid = getCurrHrefIdx();
675 if (wid >= 0) {
676 pushPosition();
677 goTo(wid);
680 break;
681 default:
683 return true;
687 bool controlKey (KeyEvent event) {
688 if (!event.pressed) return false;
689 switch (event.key) {
690 case Key.Escape:
691 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
692 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
693 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
694 if (hrefcurr >= 0) {
695 hrefcurr = -1;
696 } else {
697 ensureShipModel();
698 createQuitMenu(true);
700 refresh();
701 return true;
702 case Key.P: if (event.modifierState == ModifierState.ctrl) { concmd("r_fps toggle"); return true; } break;
703 case Key.I: if (event.modifierState == ModifierState.ctrl) { concmd("r_interference toggle"); return true; } break;
704 case Key.N: if (event.modifierState == ModifierState.ctrl) { concmd("r_sbleft toggle"); return true; } break;
705 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
706 case Key.E:
707 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
708 if (!inMenu) {
709 showShip = !showShip;
710 if (showShip) ensureShipModel(); else freeShipModel();
711 refresh();
713 return true;
715 break;
716 case Key.Q: if (event.modifierState == ModifierState.ctrl) { concmd("quit"); return true; } break;
717 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
718 case Key.S:
719 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
720 ensureShipModel();
721 createSectionMenu();
722 refresh();
724 break;
725 case Key.L:
726 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
727 ensureShipModel();
728 createRecentMenu();
729 refresh();
731 break;
732 case Key.M:
733 if (!inMenu && !showShip) {
734 inGalaxyMap = !inGalaxyMap;
735 refresh();
737 break;
738 case Key.R:
739 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
740 break;
741 case Key.Home: if (showShip) { setShip(0); return true; } break;
742 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
743 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
744 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
745 default:
747 return showShip;
750 int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
751 int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
753 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
754 int endY () { pragma(inline, true); return startY+textHeight-1; }
757 // ////////////////////////////////////////////////////////////////////////// //
758 void run () {
759 if (GWidth < MinWinWidth) GWidth = MinWinWidth;
760 if (GHeight < MinWinHeight) GHeight = MinWinHeight;
762 bookText = loadBook(bookFileName);
764 setOpenGLContextVersion(3, 0); // it's enough
765 //openGLContextCompatible = false;
767 sdwindow = new SimpleWindow(GWidth, GHeight, bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast, OpenGlOptions.yes, Resizability.allowResizing);
768 sdwindow.hideCursor(); // we will do our own
769 sdwindow.setMinSize(MinWinWidth, MinWinHeight);
771 version(X11) sdwindow.closeQuery = delegate () { concmd("quit"); };
773 auto stt = MonoTime.currTime;
774 auto prevt = MonoTime.currTime;
775 auto curt = prevt;
776 textHeight = GHeight-8;
778 MonoTime nextIfTime = MonoTime.currTime;
780 lastMMove = MonoTime.currTime;
782 auto childTid = spawn(&reformatThreadFn, thisTid);
783 childTid.setMaxMailboxSize(128, OnCrowding.block);
784 thisTid.setMaxMailboxSize(128, OnCrowding.block);
786 void loadAndFormat (string filename) {
787 assert(formatWorks <= 0);
788 bookText = null;
789 laytext = null;
790 newYLine = -1;
791 //formatWorks = -1; //FIXME
792 firstFormat = true;
793 newYFade = false;
794 toMove = 0;
795 recentFiles = null;
796 arrowDir = 0;
797 lastArrowDir = 0;
798 arrowSpeed = 0;
799 //sdwindow.redrawOpenGlSceneNow();
800 //bookText = loadBook(newBookFileName);
801 //newBookFileName = null;
802 //reformat();
803 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
804 formatWorks = 1;
805 //conwriteln("*** loading new book: '", filename, "'");
806 hreflist.unsafeArrayClear();
807 hrefcurr = -1;
808 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
809 refresh();
812 void reformat () {
813 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
814 childTid.send(ReformatWork(cast(shared)bookText, null, GWidth, GHeight));
815 refresh();
818 void formatComplete (ref ReformatWorkComplete w) {
819 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
821 auto lt = cast(LayTextC)w.laytext;
822 scope(exit) if (lt) lt.freeMemory();
823 w.laytext = null;
825 BookMetadata meta = cast(BookMetadata)w.meta;
826 w.meta = null;
828 BookText bt = cast(BookText)w.booktext;
829 w.booktext = null;
830 if (bt !is bookText) {
831 bookText = bt;
832 bookmeta = meta;
833 firstFormat = true;
834 currentSection = -666;
835 setWindowTitle(true);
836 //sdwindow.title = bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast;
837 } else if (bookmeta is null) {
838 bookmeta = meta;
841 if (isQuitRequested || formatWorks <= 0) return;
842 --formatWorks;
844 if (w.w != GWidth) {
845 if (formatWorks == 0) reformat();
846 return;
849 if (formatWorks != 0) return;
850 freeShipModel();
852 uint widx = 0;
853 if (!firstFormat && laytext !is null && laytext.lineCount) {
854 auto lidx = laytext.findLineAtY(topY);
855 if (lidx >= 0) widx = laytext.line(lidx).wstart;
857 if (laytext !is null) laytext.freeMemory();
858 laytext = lt;
859 lt = null;
860 if (firstFormat) {
861 loadState();
862 firstFormat = false;
863 doSaveCheck = false;
864 stateChanged();
865 } else {
866 goTo(widx);
868 refresh();
871 void closeWindow () {
872 doSaveState(true); // forced state save
873 if (!sdwindow.closed && vg !is null) {
874 curImg.clear();
875 curImgWhite.clear();
876 freeShipModel();
877 vg.kill();
878 vg = null;
879 sdwindow.close();
883 sdwindow.visibleForTheFirstTime = delegate () {
884 sdwindow.setAsCurrentOpenGlContext(); // make this window active
885 sdwindow.vsync = false;
886 //sdwindow.useGLFinish = false;
887 //glbindLoadFunctions();
889 try {
890 NVGContextFlag[4] flagList;
891 uint flagCount = 0;
892 if (flagNanoAA) flagList[flagCount++] = NVGContextFlag.Antialias;
893 if (flagNanoSS) flagList[flagCount++] = NVGContextFlag.StencilStrokes;
894 if (flagNanoFAA) flagList[flagCount++] = NVGContextFlag.FontAA; else flagList[flagCount++] = NVGContextFlag.FontNoAA;
895 vg = nvgCreateContext(flagList[0..flagCount]);
896 if (vg is null) {
897 conwriteln("Could not init nanovg.");
898 assert(0);
899 //sdwindow.close();
901 loadFonts(vg);
902 curImg = createCursorImage(vg);
903 curImgWhite = createCursorImage(vg, true);
904 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
905 } catch (Exception e) {
906 conwriteln("ERROR: ", e.msg);
907 concmd("quit");
908 return;
911 reformat();
913 refresh();
914 sdwindow.redrawOpenGlScene();
915 refresh();
918 sdwindow.windowResized = delegate (int w, int h) {
919 //conwriteln("w=", w, "; h=", h);
920 //if (w < MinWinWidth) w = MinWinWidth;
921 //if (h < MinWinHeight) h = MinWinHeight;
922 glViewport(0, 0, w, h);
923 GWidth = w;
924 GHeight = h;
925 textHeight = GHeight-8;
926 //reformat();
927 relayout();
928 refresh();
931 static bool isWordHref (const ref LayWord w) {
932 if (!w.style.href) return false;
933 if (auto hr = w.wordNum in bookmeta.hrefs) {
934 dstring href = hr.name;
935 if (href.length > 1 && href[0] == '#') return true;
937 return false;
940 static bool isWordHrefByIdx (int widx) {
941 if (widx < 0) return false;
942 auto w = laytext.wordByIndex(widx);
943 if (!w) return false;
944 return isWordHref(*w);
947 sdwindow.redrawOpenGlScene = delegate () {
948 if (isQuitRequested) return;
950 glconResize(GWidth/oglConScale, GHeight/oglConScale, oglConScale);
952 __gshared int cnt;
953 conwriteln("cnt=", cnt++);
956 // update window title
957 setWindowTitle();
959 //glClearColor(0, 0, 0, 0);
960 //glClearColor(0.18, 0.18, 0.18, 0);
961 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
962 glClear(glNVGClearFlags|EliteModel.glClearFlags);
964 refreshed();
965 needRedrawFlag = (fps !is null && fpsVisible);
966 if (vg is null) return;
968 scope(exit) vg.releaseImages();
969 vg.beginFrame(GWidth, GHeight, 1);
970 drawIfs(vg);
971 // draw scrollbar
973 float curHeight = (laytext !is null ? topY : 0);
974 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
975 if (th <= 0) { curHeight = 0; th = 1; }
976 float sz = cast(float)(GHeight-4)/th;
977 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
978 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
979 vg.bndScrollSlider(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
982 int owidx = -1;
983 if (laytext is null) {
984 if (shipModel is null) {
985 vg.beginPath();
986 vg.fillColor(colorText);
987 int drawY = (GHeight-textHeight)/2;
988 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
989 vg.fontFaceId(textFont);
990 vg.fontSize(fsizeText);
991 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
992 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
993 vg.fill();
995 } else if (hrefcurr >= 0) {
996 owidx = hreflist[hrefcurr].widx;
999 // draw text page
1000 int markerY = -666;
1001 hreflist.unsafeArrayClear();
1002 hrefcurr = -1;
1003 if (laytext !is null && laytext.lineCount) {
1004 vg.beginPath();
1005 vg.fillColor(colorText);
1006 int drawY = startY;
1007 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
1008 //FIXME: not GHeight!
1009 int lidx = laytext.findLineAtY(topY);
1010 if (lidx >= 0 && lidx < laytext.lineCount) {
1011 drawY -= topY-laytext.line(lidx).y;
1012 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
1013 LayFontStyle lastStyle;
1014 int startx = startX;
1015 bool setColor = true;
1016 while (lidx < laytext.lineCount && drawY < GHeight) {
1017 auto ln = laytext.line(lidx);
1018 foreach (ref LayWord w; laytext.lineWords(lidx)) {
1019 if (lastStyle != w.style || setColor) {
1020 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
1021 vg.fontSize(w.style.fontsize);
1022 auto c = NVGColor(w.style.color);
1023 vg.fillColor(c);
1024 //vg.strokeColor(c);
1025 lastStyle = w.style;
1026 setColor = false;
1028 // line highlighting
1029 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
1030 auto oid = w.objectIdx;
1031 if (oid >= 0) {
1032 vg.fill();
1033 laytext.objectAtIndex(oid).draw(vg, startx+w.x, drawY+ln.h+ln.desc);
1034 vg.beginPath();
1035 } else {
1036 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
1038 // register href
1039 if (drawY >= 0 && drawY+ln.h <= GHeight && isWordHref(w)) {
1040 HrefInfo hif;
1041 hif.widx = cast(int)w.wordNum;
1042 hif.x0 = startx+w.x;
1043 hif.y0 = drawY;
1044 hif.width = w.w;
1045 hif.height = ln.h;
1046 hreflist.unsafeArrayAppend(hif);
1048 //TODO: draw lines over whitespace
1049 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
1050 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/3, w.w, 2);
1051 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
1052 version(debug_draw) {
1053 vg.fill();
1054 vg.beginPath();
1055 vg.strokeWidth(1);
1056 vg.strokeColor(nvgRGB(0, 0, 255));
1057 vg.rect(startx+w.x, drawY, w.w, w.h);
1058 vg.stroke();
1059 vg.beginPath();
1061 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
1063 if (newYFade && newYLine == lidx) markerY = drawY;
1064 drawY += ln.h;
1065 ++lidx;
1068 vg.fill();
1071 // restore current href (if it is possible)
1072 foreach (auto idx, const ref HrefInfo hif; hreflist) if (hif.widx == owidx) { hrefcurr = cast(int)idx; break; }
1074 // draw href
1075 if (hrefcurr >= 0) {
1076 // background
1077 vg.beginPath();
1078 vg.fillColor(nvgRGBA(0, 0, 0, 42));
1079 vg.rect(hreflist[hrefcurr].x0+0.5f, hreflist[hrefcurr].y0+0.5f, hreflist[hrefcurr].width, hreflist[hrefcurr].height);
1080 vg.fill();
1081 // frame
1082 vg.beginPath();
1083 vg.strokeWidth(1);
1084 vg.strokeColor(nvgRGB(0, 0, 255));
1085 vg.setLineDash([4.0f, 4.0f]);
1086 vg.rect(hreflist[hrefcurr].x0+0.5f, hreflist[hrefcurr].y0+0.5f, hreflist[hrefcurr].width, hreflist[hrefcurr].height);
1087 vg.stroke();
1088 vg.beginPath();
1091 // draw scroll marker
1092 if (markerY != -666) {
1093 vg.beginPath();
1094 vg.fillColor(NVGColor(0.3f, 0.3f, 0.3f, newYAlpha));
1095 vg.rect(startX, markerY, GWidth, 2);
1096 vg.fill();
1099 // dim text
1100 if (!showShip) {
1101 if (colorDim.a != 1) {
1102 //vg.scissor(0, 0, GWidth, GHeight);
1103 vg.resetScissor;
1104 vg.beginPath();
1105 vg.fillColor(colorDim);
1106 vg.rect(0, 0, GWidth, GHeight);
1107 vg.fill();
1111 // dim more if menu is active
1112 if (inMenu || showShip || formatWorks != 0) {
1113 //vg.scissor(0, 0, GWidth, GHeight);
1114 vg.resetScissor;
1115 vg.beginPath();
1116 //vg.globalAlpha(0.5);
1117 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
1118 vg.rect(0, 0, GWidth, GHeight);
1119 vg.fill();
1122 if (shipModel !is null) {
1123 vg.endFrame();
1124 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
1125 zz += 10;
1126 lightsClear();
1128 lightAdd(
1129 0, 60, 60,
1130 1.0, 0.0, 0.0
1133 lightAdd(
1134 //0, 0, -60,
1135 0, 0, -zz,
1136 1.0, 1.0, 1.0
1138 drawModel(shipAngle, shipModel);
1139 vg.beginFrame(GWidth, GHeight, 1);
1140 drawShipName();
1141 //vg.endFrame();
1144 if (formatWorks == 0) {
1145 if (inMenu) {
1146 //vg.beginFrame(GWidth, GHeight, 1);
1147 //vg.scissor(0, 0, GWidth, GHeight);
1148 vg.resetScissor;
1149 currPopup.draw();
1150 //vg.endFrame();
1154 if (fps !is null && fpsVisible) {
1155 //vg.beginFrame(GWidth, GHeight, 1);
1156 //vg.scissor(0, 0, GWidth, GHeight);
1157 vg.resetScissor;
1158 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
1159 //vg.endFrame();
1162 if (inGalaxyMap) drawGalaxy(vg);
1164 // mouse cursor
1165 if (curImg.valid && !mouseHidden) {
1166 int w, h;
1167 //vg.beginFrame(GWidth, GHeight, 1);
1168 vg.beginPath();
1169 //vg.scissor(0, 0, GWidth, GHeight);
1170 vg.resetScissor;
1171 vg.imageSize(curImg, w, h);
1172 if (mouseFadingAway) {
1173 mouseAlpha -= 0.1;
1174 if (mouseAlpha <= 0) { mouseFadingAway = false; mouseHidden = true; }
1175 } else {
1176 mouseAlpha = 1.0f;
1178 vg.globalAlpha(mouseAlpha);
1179 if (!mouseHigh) {
1180 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
1181 } else {
1182 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
1184 vg.rect(mouseX, mouseY, w, h);
1185 vg.fill();
1187 if (mouseHigh) {
1188 vg.beginPath();
1189 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
1190 vg.rect(mouseX, mouseY, w, h);
1191 vg.fill();
1194 //vg.endFrame();
1196 vg.endFrame();
1197 glconDraw();
1200 void processThreads () {
1201 ReformatWorkComplete wd;
1202 for (;;) {
1203 bool workDone = false;
1204 auto res = receiveTimeout(Duration.zero,
1205 (QuitWork w) {
1206 formatWorks = -1;
1208 (ReformatWorkComplete w) {
1209 wd = w;
1210 workDone = true;
1213 if (!res) { assert(!workDone); break; }
1214 if (workDone) { workDone = false; formatComplete(wd); }
1218 auto lastTimerEventTime = MonoTime.currTime;
1219 bool somethingVisible = true;
1221 sdwindow.visibilityChanged = delegate (bool vis) {
1222 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
1223 somethingVisible = vis;
1226 conRegVar!fpsVisible("r_fps", "show fps indicator", (self, valstr) { refresh(); });
1227 conRegVar!inGalaxyMap("r_galaxymap", "show Elite galaxy map", (self, valstr) { refresh(); });
1229 conRegVar!interAllowed("r_interference", "show interference", (self, valstr) { refresh(); });
1230 conRegVar!sbLeft("r_sbleft", "show scrollbar at the left side", (self, valstr) { refresh(); });
1232 conRegVar!showShip("r_showship", "show Elite ship", (self, valstr) {
1233 if (eliteShipFiles.length == 0) return false;
1234 return true;
1236 (self, valstr) {
1237 if (showShip) ensureShipModel(); else freeShipModel();
1238 refresh();
1242 conRegFunc!(() {
1243 if (currPopup !is null) {
1244 currPopup.destroy;
1245 currPopup = null;
1246 freeShipModel();
1247 refresh();
1249 onPopupSelect = null;
1250 })("menu_close", "close current popup menu");
1252 conRegFunc!(() {
1253 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
1254 currPopup.destroy;
1255 currPopup = null;
1256 ensureShipModel();
1257 createSectionMenu();
1258 refresh();
1260 })("menu_section", "show section menu");
1262 conRegFunc!(() {
1263 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
1264 currPopup.destroy;
1265 currPopup = null;
1266 ensureShipModel();
1267 createRecentMenu();
1268 refresh();
1270 })("menu_recent", "show recent menu");
1272 sdwindow.eventLoop(1000/34,
1273 delegate () {
1274 processThreads();
1275 if (sdwindow.closed) return;
1276 conProcessQueue();
1277 if (isQuitRequested) { closeWindow(); return; }
1278 auto ctt = MonoTime.currTime;
1281 auto spass = (ctt-lastTimerEventTime).total!"msecs";
1282 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
1283 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
1284 lastTimerEventTime = ctt;
1285 // update FPS timer
1286 prevt = curt;
1287 //curt = MonoTime.currTime;
1288 curt = ctt;
1289 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
1290 auto dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
1291 if (fps !is null) fps.update(dt);
1294 // smooth scrolling
1295 if (formatWorks == 0) {
1296 if (!lastArrowDir) lastArrowDir = arrowDir;
1297 if (arrowDir && arrowDir == lastArrowDir) {
1298 arrowSpeed += 0.015;
1299 if (arrowSpeed >= 1) arrowSpeed = 1;
1300 } else if (arrowDir) {
1301 assert(lastArrowDir != arrowDir);
1302 lastArrowDir = arrowDir;
1303 arrowSpeed *= 0.6;
1304 } else {
1305 arrowSpeed -= 0.025; //*(lastArrowDir ? 4 : 1);
1306 if (arrowSpeed <= 0) {
1307 arrowSpeed = 0;
1308 if (arrowDir) {
1309 lastArrowDir = arrowDir;
1313 if (toMove != 0) {
1314 import std.math : abs;
1315 enum Delta = 92*2;
1316 immutable int sign = (toMove < 0 ? -1 : 1);
1317 // change speed
1318 static int arrowSpeed = 0;
1319 if (arrowSpeed == 0) arrowSpeed = 16;
1320 if (abs(toMove) <= arrowSpeed) {
1321 arrowSpeed /= 2;
1322 if (arrowSpeed < 4) arrowSpeed = 4;
1323 } else {
1324 arrowSpeed *= 2;
1325 if (arrowSpeed > Delta) arrowSpeed = Delta;
1327 // calc move distance
1328 int sc = arrowSpeed;
1329 if (sc > abs(toMove)) sc = abs(toMove);
1330 hardScrollBy(sc*sign);
1331 toMove -= sc*sign;
1332 if (toMove == 0) arrowSpeed = 0;
1333 nextFadeTime = ctt+500.msecs;
1334 refresh();
1337 int toMove = cast(int)(pulse.pulse(arrowSpeed)*64);
1338 //if (arrowSpeed) { import std.stdio; writeln("lastdir=", lastArrowDir, "; dir=", arrowDir, "; as=", arrowSpeed, "; toMove=", toMove); }
1339 if (toMove > 1) {
1340 toMove *= lastArrowDir;
1341 hardScrollBy(toMove);
1342 nextFadeTime = ctt+500.msecs;
1343 refresh();
1347 enum Delta = 92*2;
1348 if (toMove != 0) {
1349 import std.math : abs;
1350 immutable int sign = (toMove < 0 ? -1 : 1);
1351 // change speed
1352 if (arrowSpeed == 0) arrowSpeed = 16;
1353 if (abs(toMove) <= arrowSpeed) {
1354 arrowSpeed /= 2;
1355 if (arrowSpeed < 4) arrowSpeed = 4;
1356 } else {
1357 arrowSpeed *= 2;
1358 if (arrowSpeed > Delta) arrowSpeed = Delta;
1360 // calc move distance
1361 int sc = arrowSpeed;
1362 if (sc > abs(toMove)) sc = abs(toMove);
1363 hardScrollBy(sc*sign);
1364 toMove -= sc*sign;
1365 if (toMove == 0) arrowSpeed = 0;
1366 nextFadeTime = ctt+500.msecs;
1367 refresh();
1368 } else if (arrowDir) {
1369 if ((arrowDir < 0 && arrowSpeed > 0) || (arrowDir > 0 && arrowSpeed < 0)) arrowSpeed += arrowDir*4;
1370 arrowSpeed += arrowDir*2;
1371 if (arrowSpeed < -64) arrowSpeed = -64; else if (arrowSpeed > 64) arrowSpeed = 64;
1372 hardScrollBy(arrowSpeed);
1373 refresh();
1374 } else if (arrowSpeed != 0) {
1375 if (arrowSpeed < 0) {
1376 if ((arrowSpeed += 4) > 0) arrowSpeed = 0;
1377 } else {
1378 if ((arrowSpeed -= 4) < 0) arrowSpeed = 0;
1380 if (arrowSpeed) {
1381 hardScrollBy(arrowSpeed);
1382 refresh();
1386 // highlight fading
1387 if (newYFade) {
1388 if (ctt >= nextFadeTime) {
1389 if ((newYAlpha -= 0.1) <= 0) {
1390 newYFade = false;
1391 } else {
1392 nextFadeTime = ctt+25.msecs;
1394 refresh();
1398 // interference processing
1399 if (ctt >= nextIfTime) {
1400 import std.random : uniform;
1401 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
1402 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
1404 if (processIfs()) refresh();
1405 // ship rotation
1406 if (shipModel !is null) {
1407 shipAngle -= 1;
1408 if (shipAngle < 359) shipAngle += 360;
1409 refresh();
1411 // mouse autohide
1412 if (!mouseFadingAway) {
1413 if (!mouseHidden && !mouseHigh) {
1414 if ((ctt-lastMMove).total!"msecs" > 2500) {
1415 //mouseHidden = true;
1416 mouseFadingAway = true;
1417 mouseAlpha = 1.0f;
1418 refresh();
1422 if (somethingVisible) {
1423 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
1424 if (/*needRedraw*/true) sdwindow.redrawOpenGlSceneNow();
1425 } else {
1426 refresh();
1428 doSaveState();
1429 // load new book?
1430 if (newBookFileName.length && formatWorks == 0 && vg !is null) {
1431 doSaveState(true); // forced state save
1432 closeMenu();
1433 ensureShipModel();
1434 loadAndFormat(newBookFileName);
1435 newBookFileName = null;
1436 sdwindow.redrawOpenGlSceneNow();
1437 //refresh();
1440 delegate (KeyEvent event) {
1441 if (sdwindow.closed) return;
1442 if (glconKeyEvent(event)) return;
1443 if (event.key == Key.PadEnter) event.key = Key.Enter;
1444 if ((event.modifierState&ModifierState.numLock) == 0) {
1445 switch (event.key) {
1446 case Key.Pad0: event.key = Key.Insert; break;
1447 case Key.PadDot: event.key = Key.Delete; break;
1448 case Key.Pad1: event.key = Key.End; break;
1449 case Key.Pad2: event.key = Key.Down; break;
1450 case Key.Pad3: event.key = Key.PageDown; break;
1451 case Key.Pad4: event.key = Key.Left; break;
1452 case Key.Pad6: event.key = Key.Right; break;
1453 case Key.Pad7: event.key = Key.Home; break;
1454 case Key.Pad8: event.key = Key.Up; break;
1455 case Key.Pad9: event.key = Key.PageUp; break;
1456 //case Key.PadEnter: event.key = Key.Enter; break;
1457 default:
1460 if (controlKey(event)) return;
1461 if (menuKey(event)) return;
1462 if (readerKey(event)) return;
1465 delegate (MouseEvent event) {
1466 if (sdwindow.closed) return;
1468 int linkAt (int msx, int msy) {
1469 if (laytext !is null && bookmeta !is null) {
1470 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
1471 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
1472 if (widx >= 0) {
1473 //conwriteln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1474 auto w = laytext.wordByIndex(widx);
1475 while (widx >= 0) {
1476 //conwriteln("word #", widx, "; href=", w.style.href);
1477 if (!w.style.href) break;
1478 if (auto hr = w.wordNum in bookmeta.hrefs) {
1479 dstring href = hr.name;
1480 if (href.length > 1 && href[0] == '#') {
1481 href = href[1..$];
1482 foreach (const ref id; bookmeta.ids) {
1483 if (id.name == href) {
1484 //pushPosition();
1485 //goTo(id.wordidx);
1486 return id.wordidx;
1489 //conwriteln("id '", hr.name, "' not found!");
1490 return -1;
1493 --widx;
1494 --w;
1499 return -1;
1502 lastMMove = MonoTime.currTime;
1503 if (mouseHidden || mouseFadingAway) {
1504 mouseHidden = false;
1505 mouseFadingAway = false;
1506 mouseAlpha = 1.0f;
1507 refresh();
1509 if (mouseX != event.x || mouseY != event.y) {
1510 mouseX = event.x;
1511 mouseY = event.y;
1512 refresh();
1514 if (!menuMouse(event) && !showShip) {
1515 if (event.type == MouseEventType.buttonPressed) {
1516 switch (event.button) {
1517 case MouseButton.wheelUp: hardScrollBy(-42); break;
1518 case MouseButton.wheelDown: hardScrollBy(42); break;
1519 case MouseButton.left:
1520 auto wid = linkAt(event.x, event.y);
1521 if (wid >= 0) {
1522 pushPosition();
1523 goTo(wid);
1525 break;
1526 case MouseButton.right:
1527 popPosition();
1528 break;
1529 default:
1533 mouseHigh = (linkAt(event.x, event.y) >= 0);
1535 delegate (dchar ch) {
1536 if (sdwindow.closed) return;
1537 if (glconCharEvent(ch)) return;
1538 if (menuChar(ch)) return;
1539 //if (ch == '`') { concmd("r_console tan"); return; }
1542 closeWindow();
1544 childTid.send(QuitWork());
1545 while (formatWorks >= 0) processThreads();
1549 // ////////////////////////////////////////////////////////////////////////// //
1550 struct FlibustaUrl {
1551 string fullUrl; // onion
1552 string host;
1553 string id;
1555 this (const(char)[] aurl) {
1556 import std.format : format;
1557 aurl = aurl.xstrip();
1558 auto flibustaRE = regex(`^(?:https?://)?(?:(?:(?:www\.)?flibusta\.[^/]+)|(?:flibusta[^.]*.onion))/b/(\d+)`);
1559 auto ct = aurl.matchFirst(flibustaRE);
1560 if (!ct.empty) {
1561 fullUrl = "http://flibustaongezhld6dibs2dps6vm4nvqg2kp7vgowbu76tzopgnhazqd.onion/b/%s/fb2".format(ct[1]);
1562 id = ct[1].idup;
1563 host = "flibustaongezhld6dibs2dps6vm4nvqg2kp7vgowbu76tzopgnhazqd.onion";
1564 } else {
1565 // add protocol
1566 auto protoRE = regex(`^([^:/]+):`);
1567 auto protoMt = aurl.matchFirst(protoRE);
1568 if (protoMt.empty) fullUrl = "http:%s%s".format((aurl[0] == '/' ? "" : "//"), aurl);
1569 // add host
1570 auto hostRE = regex(`^(?:[^:/]+)://([^/]+)`);
1571 auto hostMt = fullUrl.matchFirst(hostRE);
1572 if (hostMt.empty) { fullUrl = null; return; }
1573 host = hostMt[1].idup;
1577 @property bool valid () const pure nothrow @safe @nogc { pragma(inline, true); return (fullUrl.length > 0); }
1578 @property bool isFlibusta () const pure nothrow @safe @nogc { pragma(inline, true); return (id.length > 0); }
1579 @property bool isOnion () const pure nothrow @safe @nogc { pragma(inline, true); return host.endsWithCI(".onion"); }
1583 // ////////////////////////////////////////////////////////////////////////// //
1584 string dbFindInCache() (in auto ref FlibustaUrl furl) {
1585 if (!furl.valid) return null;
1586 auto stmt = filedb.statement(`
1587 SELECT filename AS filename
1588 FROM flubusta_cache
1589 WHERE flibusta_id=:flid
1590 LIMIT 1
1593 string fname;
1594 foreach (auto row; stmt.bindText(":flid", furl.id).range) {
1595 fname = row.filename!string;
1598 if (fname.length == 0) return null;
1600 import std.file : exists;
1601 try {
1602 if (fname.exists) return fname;
1603 } catch (Exception e) {}
1605 // no disk file, delete from cache
1606 stmt = filedb.statement(`
1607 DELETE FROM flubusta_cache
1608 WHERE flibusta_id=:flid
1610 stmt.bindText(":flid", furl.id).doAll();
1612 return null;
1616 void dbPutToCache() (in auto ref FlibustaUrl furl, const(char)[] fname) {
1617 if (!furl.valid || fname.length == 0 || furl.id.length == 0) return;
1619 auto stmt = filedb.statement(`
1620 INSERT INTO flubusta_cache
1621 ( flibusta_id, filename)
1622 VALUES(:flibusta_id,:filename)
1623 ON CONFLICT(flibusta_id)
1624 DO UPDATE
1625 SET filename=:filename
1627 stmt
1628 .bindText(":flibusta_id", furl.id)
1629 .bindText(":filename", fname)
1630 .doAll();
1634 // ////////////////////////////////////////////////////////////////////////// //
1635 // returns file name
1636 string fileDown() (in auto ref FlibustaUrl furl) {
1637 if (!furl.valid || !furl.isFlibusta) return null;
1639 string cachedFName = dbFindInCache(furl);
1640 if (cachedFName.length) return cachedFName;
1642 // content-disposition: attachment; filename="Divov_Sled-zombi.1lzb6Q.96382.fb2.zip"
1643 auto cdRE0 = regex(`^\s*attachment\s*;\s*filename="(.+?)"`, "i");
1644 auto cdRE1 = regex(`^\s*attachment\s*;\s*filename=([^;]+?)`, "i");
1646 auto http = HTTP(furl.host);
1647 http.method = HTTP.Method.get;
1648 http.url = furl.fullUrl;
1650 string fname = null;
1651 string tmpfname = null;
1652 string fnps = null;
1653 bool alreadyDowned = false;
1654 VFile fo;
1656 http.onReceiveHeader = delegate (in char[] key, in char[] value) {
1657 //writeln(key ~ ": " ~ value);
1658 if (key.strEquCI("content-disposition")) {
1659 auto ct = value.matchFirst(cdRE0);
1660 if (ct.empty) ct = value.matchFirst(cdRE1);
1661 if (ct[1].length) {
1662 auto fnp = ct[1].xstrip;
1663 auto lslpos = fnp.lastIndexOf('/');
1664 if (lslpos > 0) fnp = fnp[lslpos+1..$];
1665 if (fnp.length == 0) {
1666 fname = null;
1667 } else {
1668 import std.file : exists, mkdirRecurse;
1669 import std.path;
1670 string xfn = buildPath(RcDir, "cache");
1671 xfn.mkdirRecurse();
1672 char[] xxname;
1673 xxname.reserve(fnp.length);
1674 foreach (char ch; fnp) {
1675 if (ch <= ' ' || ch == 127) ch = '_';
1676 xxname ~= ch;
1678 fnps = cast(string)xxname; // it is safe to cast here
1679 fname = buildPath(xfn, fnps);
1680 tmpfname = fname~".down.part";
1682 if (fname.exists) {
1683 alreadyDowned = true;
1684 throw new Exception("already here");
1685 //throw new FileAlreadyDowned("already here");
1688 //write("\r", fnp, " [", furl.fullUrl, "]\e[K");
1694 http.onReceive = delegate (ubyte[] data) {
1695 if (!fo.isOpen) {
1696 if (fname.length == 0) throw new Exception("no file name found in headers");
1697 //writeln(" downloading to ", fname);
1698 fo = VFile(tmpfname, "w");
1700 fo.rawWriteExact(data);
1701 return data.length;
1704 MonoTime lastProgTime = MonoTime.zero;
1705 enum BarLength = 68;
1706 bool doProgUpdate = true;
1707 char[1024] buf = void;
1708 int oldDots = -1, oldPrc = -1;
1709 uint bufpos = 0;
1710 int stickPos = 1;
1711 immutable string stickStr = `|/-\`;
1713 // will set `doProgUpdate`, and update `oldXXX`
1714 void buildPBar (usize dlTotal, usize dlNow) {
1715 void put (const(char)[] s...) nothrow {
1716 if (s.length == 0) return;
1717 if (bufpos >= buf.length) return;
1718 int left = cast(int)buf.length-bufpos;
1719 if (s.length > left) s = s[0..left];
1720 assert(s.length > 0);
1721 import core.stdc.string : memcpy;
1722 memcpy(buf.ptr+bufpos, s.ptr, s.length);
1723 bufpos += cast(int)s.length;
1725 void putprc (int prc) {
1726 if (prc < 0) prc = 0; else if (prc > 100) prc = 100;
1727 if (bufpos >= buf.length || buf.length-bufpos < 5) return; // oops
1728 import core.stdc.stdio;
1729 bufpos += cast(int)snprintf(buf.ptr+bufpos, 5, "%3d%%", prc);
1731 void putCommaNum (usize n, usize max=0) {
1732 char[128] buf = void;
1733 if (max < n) {
1734 put(intWithCommas(buf[], n));
1735 } else {
1736 auto len = intWithCommas(buf[], max).length;
1737 auto pt = intWithCommas(buf[], n);
1738 while (len-- > pt.length) put(" ");
1739 put(pt);
1742 bufpos = 0;
1743 put("\r");
1744 put(fnps);
1745 put(" [");
1746 auto barpos = bufpos;
1747 foreach (immutable _; 0..BarLength) put(" ");
1748 put("]");
1749 if (dlTotal > 0) {
1750 int prc = cast(int)(cast(ulong)100*dlNow/dlTotal);
1751 if (prc < 0) prc = 0; else if (prc > 100) prc = 100;
1752 int dots = cast(int)(cast(ulong)BarLength*dlNow/dlTotal);
1753 if (dots < 0) dots = 0; else if (dots > BarLength) dots = BarLength;
1754 if (prc != oldPrc || dots != oldDots) {
1755 doProgUpdate = true;
1756 oldPrc = prc;
1757 oldDots = dots;
1759 put(" [");
1760 putCommaNum(dlNow, dlTotal);
1761 put("/");
1762 putCommaNum(dlTotal);
1763 put("] ");
1764 putprc(prc);
1765 // dots
1766 foreach (immutable dp; 0..dots) if (barpos+dp < buf.length) buf[barpos+dp] = '.';
1767 } else {
1768 put("?\e[K");
1769 if (oldDots != -1 || oldPrc != -1) doProgUpdate = true;
1770 oldDots = -1;
1771 oldPrc = -1;
1775 http.onProgress = delegate (usize dltotal, usize dlnow, usize ultotal, usize ulnow) {
1776 //writeln("Progress ", dltotal, ", ", dlnow, ", ", ultotal, ", ", ulnow);
1777 if (fname.length == 0) {
1778 auto ct = MonoTime.currTime;
1779 if ((ct-lastProgTime).total!"msecs" >= 100) {
1780 write("\x08", stickStr[stickPos]);
1781 stickPos = (stickPos+1)%cast(int)stickStr.length;
1782 lastProgTime = ct;
1784 return 0;
1786 buildPBar(dltotal, dlnow);
1787 if (doProgUpdate) { write("\e[?7l", buf[0..bufpos], "\e[K\e[?7h"); doProgUpdate = false; }
1788 //if (dltotal == 0) return 0;
1789 //auto ct = MonoTime.currTime;
1790 //if ((ct-lastProgTime).total!"msecs" < 1000) return 0;
1791 //lastProgTime = ct;
1792 //writef("\r%s [%s] -- [%s/%s] %3u%%\e[K", fnps, host, intWithCommas(dlnow), intWithCommas(dltotal), 100UL*dlnow/dltotal);
1793 return 0;
1796 if (furl.isOnion) {
1797 http.proxyType = HTTP.CurlProxy.socks5_hostname;
1798 http.proxy = "127.0.0.1";
1799 http.proxyPort = 9050;
1802 try {
1803 //write("downloading from [", host, "]: ", realUrl, " ... ");
1804 write("downloading from Flibusta: ", furl.fullUrl, " ... ", stickStr[0]);
1805 http.perform();
1806 if (fo.isOpen) {
1807 buildPBar(cast(uint)fo.size, cast(uint)fo.size);
1808 } else {
1809 buildPBar(1, 1);
1811 writeln(buf[0..bufpos], "\e[K");
1812 //write("\r\e[K");
1813 } catch (Exception e) {
1814 if (/*cast(FileAlreadyDowned)e*/alreadyDowned) {
1815 write("\r", fname, " already downloaded.\e[K");
1816 return fname; // already here
1818 if (tmpfname.length) {
1819 import std.exception : collectException;
1820 import std.file : remove;
1821 collectException(tmpfname.remove);
1823 throw e;
1826 if (fo.isOpen) {
1827 // something was downloaded, rename it
1828 import std.file : rename;
1829 fo.close();
1830 rename(tmpfname, fname);
1831 dbPutToCache(furl, fname);
1832 return fname;
1835 return null;
1839 // ////////////////////////////////////////////////////////////////////////// //
1840 void main (string[] args) {
1841 import std.path;
1843 conRegVar!oglConScale(1, 4, "r_conscale", "console scale");
1845 conProcessQueue(256*1024); // load config
1846 conProcessArgs!true(args);
1847 conProcessQueue(256*1024);
1849 universe = Galaxy(0);
1851 if (args.length == 1) {
1852 string lnn = getLatestFileName();
1853 if (lnn.length) args ~= lnn;
1854 } else {
1855 if (args.length != 2) assert(0, "invalid number of arguments");
1856 auto furl = FlibustaUrl(args[1]);
1857 if (furl.valid && furl.isFlibusta) {
1858 string fn = fileDown(furl);
1859 if (fn.length == 0) assert(0, "can't download file");
1860 args[1] = fn;
1864 if (args.length == 1) assert(0, "no filename");
1866 readConfig();
1868 bookFileName = args[1];
1869 run();