use unicode classifiers
[xreader.git] / xreader.d
blob6a198ed2e6a03457cc9aa0c86294831a95bf272e
1 module xreader /*is aliced*/;
3 import core.atomic;
4 import core.time;
6 import std.concurrency;
7 import std.stdio;
9 import iv.strex;
11 import arsd.simpledisplay;
12 import arsd.color;
13 import arsd.png;
14 import arsd.jpeg;
16 import iv.nanovg;
17 import iv.nanovg.oui.blendish;
18 //import iv.nanovg.svg;
19 import iv.nanovg.perf;
21 import iv.vfs;
23 import elite;
24 import xiniz;
26 import booktext;
29 // ////////////////////////////////////////////////////////////////////////// //
30 __gshared string RcDir = "~/.xreader";
31 __gshared string stateFileName;
33 __gshared int rotx, rotz;
34 __gshared bool showShip = false;
37 // ////////////////////////////////////////////////////////////////////////// //
38 __gshared string textFontName = "~/ttf/ms/arial.ttf:noaa";
39 __gshared string epiFontName = "~/ttf/ms/ariali.ttf:noaa";
40 __gshared string uiFontName = "~/ttf/ms/verdana.ttf:noaa";
41 __gshared int GWidth = 900;
42 __gshared int GHeight = 1024;
43 __gshared int fSize = 24;
44 __gshared bool sbLeft = false;
45 __gshared bool interAllowed = false;
47 __gshared bool flagNanoAA = false;
48 __gshared bool flagNanoSS = false;
50 __gshared uint colorDim = 0;
51 __gshared uint colorBack = 0x2a2a2a;
52 __gshared uint colorText = 0xff7f00;
53 __gshared uint colorTextHi = 0xffff00;
56 // ////////////////////////////////////////////////////////////////////////// //
57 auto u2ca (uint v) {
58 return nvgRGBA(
59 (v>>16)&0xff,
60 (v>>8)&0xff,
61 v&0xff,
62 (v>>24)&0xff,
67 auto u2c (uint v) {
68 return nvgRGB(
69 (v>>16)&0xff,
70 (v>>8)&0xff,
71 v&0xff,
76 // ////////////////////////////////////////////////////////////////////////// //
77 VFile fwritef(A...) (VFile fl, string fmt, /*lazy*/ A args) {
78 import std.string : format;
79 auto s = format(fmt, args);
80 if (s.length) fl.rawWriteExact(s[]);
81 return fl;
85 // ////////////////////////////////////////////////////////////////////////// //
86 void readConfig () {
87 import std.path;
88 VFile fl;
89 try {
90 fl = VFile(buildPath(RcDir, ".config.ui"));
91 } catch (Exception) {
92 try {
93 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
94 fl = VFile(buildPath(RcDir, ".config.ui"), "w");
95 fl.fwritef("width=%s\n", GWidth);
96 fl.fwritef("height=%s\n", GHeight);
97 fl.fwritef("font-text=%s\n", textFontName);
98 fl.fwritef("font-epi=%s\n", epiFontName);
99 fl.fwritef("font-ui=%s\n", uiFontName);
100 fl.fwritef("text-size=%s\n", fSize);
101 fl.fwritef("scrollbar-on-left=%s\n", sbLeft);
102 fl.fwritef("interference=%s\n", interAllowed);
103 fl.fwritef("color-dim=0x%08x\n", colorDim);
104 fl.fwritef("color-back=0x%08x\n", colorBack);
105 fl.fwritef("color-text=0x%08x\n", colorText);
106 fl.fwritef("color-text-hi=0x%08x\n", colorTextHi);
107 fl.fwritef("nano-aa=%s\n", flagNanoAA);
108 fl.fwritef("nano-ss=%s\n", flagNanoSS);
109 } catch (Exception) {}
110 return;
112 xiniParse(fl,
113 "font-text", &textFontName,
114 "font-ui", &uiFontName,
115 "width", &GWidth,
116 "height", &GHeight,
117 "text-size", &fSize,
118 "scrollbar-on-left", &sbLeft,
119 "interference", &interAllowed,
120 "color-dim", &colorDim,
121 "color-back", &colorBack,
122 "color-text", &colorText,
123 "color-text-hi", &colorTextHi,
124 "nano-aa", &flagNanoAA,
125 "nano-ss", &flagNanoSS,
130 // ////////////////////////////////////////////////////////////////////////// //
131 bool isComment (const(char)[] s) {
132 while (s.length && s.ptr[0] <= ' ') s = s[1..$];
133 return (s.length > 0 && s.ptr[0] == '#');
137 // ////////////////////////////////////////////////////////////////////////// //
138 shared int formatWorks = -1;
139 //__gshared NVGContext vg = null;
140 __gshared int textFontF, epiFontF, uiFontF;
143 struct ReformatWork {
144 shared BookText booktext;
145 int w, h;
146 shared NVGContext fvg;
149 struct ReformatWorkDone {
150 //BookText booktext;
151 int w, h;
152 shared(TextLine[]) lines;
155 struct QuitWork {
159 void reformatThreadFn (Tid ownerTid) {
160 NVGContext vg;
162 void textWH (const(char)[] text, TextLine.Text type, out int wdt, out int hgt) {
163 float[4] bounds;
164 final switch (type) {
165 case TextLine.Text.Normal: vg.fontFaceId(textFontF); break;
166 case TextLine.Text.Heading: vg.fontFaceId(textFontF); break;
167 case TextLine.Text.Epigraph: vg.fontFaceId(epiFontF); break;
169 //vg.fontFaceId(textFont);
170 vg.fontSize(fSize);
171 vg.textBounds(0, 0, text, bounds[]);
172 wdt = cast(int)(bounds[2]-bounds[0]);
173 hgt = cast(int)(bounds[3]-bounds[1]);
176 bool doQuit = false;
177 ReformatWork cw;
178 while (!doQuit) {
179 cw = cw.init;
180 receive(
181 (ReformatWork w) {
182 cw = w;
184 (QuitWork w) {
185 doQuit = true;
188 if (!doQuit && cw.w > 0 && cw.h > 0 && cw.booktext !is null) {
189 int maxWidth = cw.w-4-2-BND_SCROLLBAR_WIDTH-2;
190 vg = cast(NVGContext)cw.fvg;
191 auto stt = MonoTime.currTime;
192 auto lines = TextLine.format(cast()cw.booktext, maxWidth, &textWH);
193 writeln("reformatted in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds");
194 vg = null;
195 auto res = ReformatWorkDone(cw.w, cw.h, cast(shared)lines);
196 send(ownerTid, res);
199 send(ownerTid, QuitWork());
203 void run (BookText booktext) {
204 import core.time;
206 static struct Interference {
207 int y, ey, speed;
210 NVGContext vg = null;
211 NVGContext fvg = null;
212 PerfGraph fps;
213 bool fpsVisible = false;
215 if (GWidth < 32) GWidth = 32;
216 if (GHeight < 32) GHeight = 32;
218 int textFont, epiFont, uiFont;
219 int toMove = 0;
221 struct Pos {
222 int sec, par, word;
224 Pos[] posstack;
226 void loadFonts () {
227 import std.algorithm : endsWith;
228 import std.path : expandTilde;
229 string tfn = "text";
230 string ufn = "ui";
231 string efn = "textepi";
232 if (textFontName.endsWith(":noaa")) { textFontName = textFontName[0..$-5]; tfn ~= ":noaa"; }
233 if (epiFontName.endsWith(":noaa")) { epiFontName = epiFontName[0..$-5]; efn ~= ":noaa"; }
234 if (uiFontName.endsWith(":noaa")) { uiFontName = uiFontName[0..$-5]; ufn ~= ":noaa"; }
236 textFont = vg.createFont(tfn, textFontName.expandTilde);
237 if (textFont < 0) throw new Exception("can't load text font: '"~textFontName~"'");
238 epiFont = vg.createFont(tfn, epiFontName.expandTilde);
239 if (epiFont < 0) throw new Exception("can't load epi font: '"~epiFontName~"'");
240 uiFont = vg.createFont(ufn, uiFontName.expandTilde);
241 if (uiFont < 0) throw new Exception("can't load text font: '"~uiFontName~"'");
242 bndSetFont(uiFont);
244 textFontF = fvg.createFont(tfn, textFontName.expandTilde);
245 if (textFontF < 0) throw new Exception("can't load text font: '"~textFontName~"'");
246 epiFontF = fvg.createFont(tfn, epiFontName.expandTilde);
247 if (epiFontF < 0) throw new Exception("can't load epi font: '"~epiFontName~"'");
248 uiFontF = fvg.createFont(ufn, uiFontName.expandTilde);
249 if (uiFontF < 0) throw new Exception("can't load text font: '"~uiFontName~"'");
252 bool doQuit = false;
254 //setOpenGLContextVersion(3, 2); // up to GLSL 150
255 setOpenGLContextVersion(2, 0); // it's enough
256 //openGLContextCompatible = false;
258 //writeln(booktext.title~" -- "~booktext.authorFirst~" "~booktext.authorLast);
259 auto sdwindow = new SimpleWindow(GWidth, GHeight, booktext.title~" -- "~booktext.authorFirst~" "~booktext.authorLast, OpenGlOptions.yes, Resizablity.allowResizing);
260 //sdwindow.hideCursor();
261 //scope(exit) sdwindow.close();
263 auto stt = MonoTime.currTime;
264 auto prevt = MonoTime.currTime;
265 auto curt = prevt;
266 float dt = 0, secs = 0;
267 //int mxOld = -1, myOld = -1;
268 TextLine[] lines;
269 int topLine = 0;
270 int topYOfs = 0;
271 int textHeight = GHeight-8;
272 int totalHeight = 0;
273 bool doSaveCheck = false;
274 MonoTime nextSaveTime;
275 bool needRedraw = true;
276 int newYLine = -1;
277 float newYAlpha;
278 bool newYFade = false;
279 MonoTime nextFadeTime;
280 Interference[] ifs;
282 bool firstFormat = true;
284 auto childTid = spawn(&reformatThreadFn, thisTid);
286 void processIfs () {
287 foreach (ref l; ifs) {
288 if (l.speed == 0) continue;
289 needRedraw = true;
290 l.y += l.speed;
291 if (l.speed < 0) {
292 if (l.y < l.ey) l.speed = 0;
293 } else {
294 if (l.y > l.ey) l.speed = 0;
299 void drawIfs () {
300 foreach (ref l; ifs) {
301 if (l.speed == 0) continue;
302 vg.beginPath();
303 vg.fillColor(nvgRGB(0, 40, 0));
304 vg.rect(0, l.y-1, GWidth, 3);
305 vg.fill();
309 void addIf () {
310 import std.random : uniform;
311 if (interAllowed) {
312 int idx = 0;
313 while (idx < ifs.length && ifs[idx].speed != 0) ++idx;
314 if (idx >= ifs.length) {
315 if (ifs.length > 10) return;
316 ifs ~= Interference();
318 if (uniform!"[]"(0, 1) == 0) {
319 // up
320 ifs[idx].y = GHeight/2+uniform!"[]"(50, GHeight/2-100);
321 int ty = ifs[idx].y-40;
322 if (ty < 0) return;
323 ifs[idx].ey = uniform!"[]"(0, ty);
324 ifs[idx].speed = -uniform!"[]"(1, 25);
325 } else {
326 // down
327 ifs[idx].y = GHeight/2-uniform!"[]"(50, GHeight/2-100);
328 int ty = ifs[idx].y+40;
329 if (ty >= GHeight) return;
330 ifs[idx].ey = uniform!"[]"(ty, GHeight);
331 ifs[idx].speed = uniform!"[]"(1, 25);
333 needRedraw = true;
337 void doSaveState (bool forced=false) {
338 if (!forced) {
339 if (atomicLoad(formatWorks) != 0) return;
340 if (!doSaveCheck) return;
341 auto ct = MonoTime.currTime;
342 if (ct < nextSaveTime) return;
343 } else {
344 if (lines.length == 0) return;
346 try {
347 auto fo = File(stateFileName, "w");
348 if (lines.length) {
349 auto tl = (topLine < 0 ? 0 : (topLine >= lines.length ? cast(int)lines.length-1 : topLine));
350 fo.writeln("section=", lines[tl].sec);
351 fo.writeln("paragraph=", lines[tl].par);
352 fo.writeln("word=", lines[tl].word);
354 } catch (Exception) {}
355 doSaveCheck = false;
358 void stateChanged () {
359 if (!doSaveCheck) {
360 doSaveCheck = true;
361 nextSaveTime = MonoTime.currTime+10.seconds;
365 void hardScrollBy (int delta) {
366 if (delta == 0 || lines.length == 0) return;
367 scope(exit) {
368 if (topLine >= lines.length) topLine = cast(int)lines.length-1;
369 if (topLine < 0) topLine = 0;
370 //curHeight = lines[topLine].y;
371 stateChanged();
372 needRedraw = true;
374 if (delta < 0) {
375 if (topLine < 0) { topYOfs = 0; topLine = 0; return; }
376 delta = -delta;
377 if (topYOfs > 0) {
378 if (delta <= topYOfs) { topYOfs -= delta; return; }
379 delta -= topYOfs;
380 topYOfs = 0;
382 while (delta > 0 && topLine > 0) {
383 int sc = lines.ptr[topLine].height;
384 if (sc > delta) {
385 topYOfs = sc-delta;
386 --topLine;
387 break;
388 } else {
389 delta -= sc;
390 --topLine;
393 } else {
394 if (topLine >= lines.length) { topYOfs = 0; return; }
395 delta += topYOfs;
396 topYOfs = 0;
397 while (delta > 0 && topLine < lines.length-1) {
398 int sc = lines.ptr[topLine].height;
399 if (sc > delta) {
400 topYOfs = delta;
401 break;
402 } else {
403 delta -= sc;
404 ++topLine;
410 void scrollBy (int delta) {
411 if (delta == 0 || lines.length == 0) return;
412 toMove += delta;
413 newYFade = true;
414 newYAlpha = 1;
415 if (toMove < 0) {
417 int newY = lines[topLine].y+lines[topLine].height/2;
418 int n = -toMove;
419 int ln = topLine;
420 while (n > 0 && ln < lines.length) {
421 if ((n -= lines[ln].height) <= 0) break;
422 newY += lines[ln].height;
423 ++ln;
425 newYLine = ln;
426 //if (ln < lines.length) assert(newY >= lines[ln].y && newY < lines[ln].y+lines[ln].height);
428 newYLine = topLine;
429 } else {
430 // go down
431 int newY = lines[topLine].y+lines[topLine].height/2;
432 int n = textHeight;
433 int ln = topLine;
434 while (n > 0 && ln < lines.length) {
435 if ((n -= lines[ln].height) <= 0) break;
436 newY += lines[ln].height;
437 ++ln;
439 newYLine = ln;
440 if (ln < lines.length) {
441 //assert(newY == lines[ln].y);
442 //newY += lines[ln].height/2;
447 void goHome () {
448 if (topLine == 0 && topYOfs == 0) return;
449 topLine = 0;
450 topYOfs = 0;
451 stateChanged();
452 needRedraw = true;
455 void goTo (int osec, int opar, int oidx) {
456 //writeln("goto: sec=", osec, "; par=", opar, "; word=", oidx);
457 topLine = 0;
458 if (lines.length) {
459 while (topLine < lines.length) {
460 if (lines[topLine].sec == osec && lines[topLine].par == opar) {
461 // i found her!
462 auto pl = topLine;
463 while (topLine < lines.length && lines[topLine].sec == osec && lines[topLine].par == opar && lines[topLine].word < oidx) ++topLine;
464 if (topLine > pl && topLine < lines.length && lines[topLine].sec == osec && lines[topLine].par == opar) {
465 if (lines[topLine].word > oidx) --topLine;
466 } else {
467 topLine = pl;
469 break;
471 ++topLine;
473 } else {
474 topLine = 0;
476 topYOfs = 0;
477 needRedraw = true;
480 void loadState () {
481 try {
482 int sec = -1, par = -1, word = -1;
483 xiniParse(VFile(stateFileName),
484 "section", &sec,
485 "paragraph", &par,
486 "word", &word,
488 if (sec >= 0 && par >= 0 && word >= 0) goTo(sec, par, word);
489 } catch (Exception) {}
492 // reformat, and fix position
493 void reformat () {
494 vg.fontFaceId(textFont);
495 vg.fontSize(fSize);
497 //atomicStore(reformatComplete, false);
498 if (atomicLoad(formatWorks) < 0) {
499 atomicStore(formatWorks, 1);
500 } else {
501 atomicOp!"+="(formatWorks, 1);
504 childTid.send(ReformatWork(cast(shared)booktext, GWidth, GHeight, cast(shared)fvg));
507 void formatComplete (in ref ReformatWorkDone w) {
508 if (doQuit || atomicLoad(formatWorks) <= 0) return;
509 atomicOp!"-="(formatWorks, 1);
511 if (w.w != GWidth) {
512 if (atomicLoad(formatWorks) == 0) reformat();
513 return;
516 if (atomicLoad(formatWorks) != 0) return;
518 if (!firstFormat && topLine >= 0 && topLine < lines.length) {
519 int osec = lines[topLine].sec;
520 int opar = lines[topLine].par;
521 int oidx = lines[topLine].word;
522 lines = cast(TextLine[])w.lines;
523 goTo(osec, opar, oidx);
524 } else if (firstFormat) {
525 lines = cast(TextLine[])w.lines;
526 loadState();
527 firstFormat = false;
528 doSaveCheck = false;
529 stateChanged();
530 } else {
531 auto otl = topLine;
532 lines = cast(TextLine[])w.lines;
533 if (otl && lines.length) topLine = cast(int)lines.length-1; else topLine = 0;
534 topYOfs = 0;
536 if (topLine >= lines.length) topLine = cast(int)(lines.length ? lines.length-1 : 0);
537 totalHeight = 0;
538 if (lines.length) {
539 totalHeight = lines[$-1].y;
540 //curHeight = lines[topLine].y;
542 if (totalHeight < 1) totalHeight = 1;
543 needRedraw = true;
547 sdwindow.closeQuery = delegate () { doQuit = true; };
549 void closeWindow () {
550 doSaveState(true); // forced state save
551 if (!sdwindow.closed && vg !is null) {
552 //vg.deleteImage(vgimg);
553 //vgimg = -1;
554 vg.deleteGL2();
555 vg = null;
556 sdwindow.close();
560 sdwindow.visibleForTheFirstTime = delegate () {
561 sdwindow.setAsCurrentOpenGlContext(); // make this window active
562 sdwindow.vsync = false;
563 //sdwindow.useGLFinish = false;
564 //glbindLoadFunctions();
566 try {
567 uint flags = NVG_DEBUG;
568 if (flagNanoAA) flags |= NVG_ANTIALIAS;
569 if (flagNanoSS) flags |= NVG_STENCIL_STROKES;
570 vg = createGL2NVG(flags);
571 if (vg is null) {
572 import std.stdio;
573 writeln("Could not init nanovg.");
574 assert(0);
575 //sdwindow.close();
577 fvg = createGL2NVG(flags);
578 if (fvg is null) {
579 import std.stdio;
580 writeln("Could not init nanovg.");
581 assert(0);
582 //sdwindow.close();
584 loadFonts();
585 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
586 } catch (Exception e) {
587 import std.stdio : stderr;
588 stderr.writeln("ERROR: ", e.msg);
589 doQuit = true;
590 return;
593 reformat();
594 //loadState();
596 needRedraw = true;
597 sdwindow.redrawOpenGlScene();
598 needRedraw = true;
601 sdwindow.windowResized = delegate (int w, int h) {
602 //writeln("w=", w, "; h=", h);
603 glViewport(0, 0, w, h);
604 GWidth = w;
605 GHeight = h;
606 textHeight = GHeight-8;
607 reformat();
610 string[] sectionNames;
612 void cacheSectionNames () {
613 if (sectionNames.length != booktext.secs.length) {
614 sectionNames.length = 0;
615 sectionNames.reserve(booktext.secs.length);
616 foreach (BookText.Section sec; booktext.secs) {
617 string s;
618 if (sec.title.length) {
619 foreach (string w; sec.title[0].words) {
620 bool addSpace = (s.length > 0);
621 if (addSpace && s[$-1] == '-') {
622 if (s.length < 2 || s[$-2] == '-') addSpace = false;
624 if (addSpace) s ~= ' ';
625 s ~= w;
628 sectionNames ~= s;
633 bool inSectionMenu = false;
634 int curSectionNum = -1;
635 int topSectionNum = 0;
636 int maxSectionsPerPage = 0;
638 void drawSectionMenu () {
639 int mx = 10, my = 10;
640 vg.fontFaceId(uiFont);
641 vg.textAlign(NVGAlign.Left|NVGAlign.Baseline);
642 vg.fontSize(16);
643 float w = 0, h = 0;
644 int maxSects = 0;
645 cacheSectionNames();
646 if (curSectionNum < 0) {
647 // find current section
648 curSectionNum = 0;
649 if (topLine >= 0 && topLine < lines.length) {
650 curSectionNum = lines[topLine].sec;
653 bool noMoreH = false;
654 foreach (string s; sectionNames) {
655 auto bw = vg.bndLabelWidth(-1, s);
656 if (bw > w) w = bw;
657 if (!noMoreH) {
658 h += BND_WIDGET_HEIGHT;
659 ++maxSects;
660 if (h+BND_WIDGET_HEIGHT+16 >= GHeight-my) noMoreH = true;
663 maxSectionsPerPage = maxSects;
664 // make current item visible
665 if (curSectionNum >= 0 && curSectionNum < sectionNames.length) {
666 if (curSectionNum < topSectionNum) {
667 topSectionNum = curSectionNum;
668 } else if (topSectionNum+maxSects <= curSectionNum) {
669 topSectionNum = curSectionNum-maxSects+1;
670 if (topSectionNum < 0) topSectionNum = 0;
673 vg.bndMenuBackground(mx, my, w+16, h+16, BND_CORNER_NONE);
674 h = 8;
675 foreach (immutable idx; 0..maxSects) {
676 int sn = cast(int)(topSectionNum+idx);
677 if (sn >= 0) {
678 if (sn >= sectionNames.length) break;
679 vg.bndMenuItem(mx+8, my+h, w, BND_WIDGET_HEIGHT, (curSectionNum == sn ? BND_ACTIVE : BND_DEFAULT), -1, sectionNames.ptr[sn]);
681 h += BND_WIDGET_HEIGHT;
685 void pushPosition () {
686 if (lines.length == 0 || topLine < 0 || topLine >= lines.length) return;
687 posstack ~= Pos(
688 lines[topLine].sec,
689 lines[topLine].par,
690 lines[topLine].word,
694 void popPosition () {
695 if (posstack.length == 0) return;
696 auto pos = posstack[$-1];
697 posstack.length -= 1;
698 posstack.assumeSafeAppend;
699 goTo(pos.sec, pos.par, pos.word);
702 void gotoSection (int sn) {
703 if (sn < 0 || sn >= booktext.secs.length) return;
704 foreach (immutable idx, TextLine line; lines) {
705 if (line.sec == sn) {
706 pushPosition();
707 topLine = cast(int)idx;
708 topYOfs = 0;
709 needRedraw = true;
710 return;
715 bool sectionMenuKey (KeyEvent event) {
716 if (atomicLoad(formatWorks) != 0) return false;
717 if (!inSectionMenu) return false;
718 if (!event.pressed) return false;
719 switch (event.key) {
720 case Key.Escape:
721 inSectionMenu = false;
722 needRedraw = true;
723 break;
724 case Key.Up:
725 if (booktext.secs.length) {
726 --curSectionNum;
727 if (curSectionNum < 0) curSectionNum = cast(int)booktext.secs.length-1;
728 needRedraw = true;
730 break;
731 case Key.Down:
732 if (booktext.secs.length) {
733 ++curSectionNum;
734 if (curSectionNum >= booktext.secs.length) curSectionNum = 0;
735 needRedraw = true;
737 break;
738 case Key.PageUp:
739 if (booktext.secs.length) {
740 if (maxSectionsPerPage > 1) curSectionNum -= maxSectionsPerPage-1; else curSectionNum = 0;
741 if (curSectionNum < 0) curSectionNum = 0;
742 needRedraw = true;
744 break;
745 case Key.PageDown:
746 if (booktext.secs.length) {
747 if (maxSectionsPerPage > 1) curSectionNum += maxSectionsPerPage-1; else curSectionNum = cast(int)booktext.secs.length-1;
748 if (curSectionNum >= booktext.secs.length) curSectionNum = cast(int)booktext.secs.length-1;
749 needRedraw = true;
751 break;
752 case Key.Home:
753 curSectionNum = 0;
754 needRedraw = true;
755 break;
756 case Key.End:
757 if (booktext.secs.length) {
758 curSectionNum = cast(int)booktext.secs.length-1;
759 needRedraw = true;
761 break;
762 case Key.Enter:
763 inSectionMenu = false;
764 needRedraw = true;
765 gotoSection(curSectionNum);
766 break;
767 default:
769 return true;
772 bool quitMenuYes = true;
773 bool inQuitMenu = false;
775 void drawQuitMenu () {
776 int mx = 10, my = 10;
777 vg.fontFaceId(uiFont);
778 vg.textAlign(NVGAlign.Left|NVGAlign.Baseline);
779 vg.fontSize(16);
780 float w = 0, h = BND_WIDGET_HEIGHT*3;
782 auto bw = vg.bndLabelWidth(-1, "Quit?");
783 if (bw > w) w = bw;
786 auto bw = vg.bndLabelWidth(-1, "Yes");
787 if (bw > w) w = bw;
790 auto bw = vg.bndLabelWidth(-1, "No");
791 if (bw > w) w = bw;
793 vg.bndMenuBackground(mx, my, w+16, h+16, BND_CORNER_NONE);
794 h = 8;
795 vg.bndMenuItem(mx+8, my+8+BND_WIDGET_HEIGHT*0, w, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, "Quit?");
796 vg.bndMenuItem(mx+8, my+8+BND_WIDGET_HEIGHT*1, w, BND_WIDGET_HEIGHT, (quitMenuYes ? BND_ACTIVE : BND_DEFAULT), -1, "Yes");
797 vg.bndMenuItem(mx+8, my+8+BND_WIDGET_HEIGHT*2, w, BND_WIDGET_HEIGHT, (!quitMenuYes ? BND_ACTIVE : BND_DEFAULT), -1, "No");
800 bool quitMenuKey (KeyEvent event) {
801 if (atomicLoad(formatWorks) != 0) return false;
802 if (!inQuitMenu) return false;
803 if (!event.pressed) return false;
804 switch (event.key) {
805 case Key.Escape:
806 inQuitMenu = false;
807 needRedraw = true;
808 break;
809 case Key.Up:
810 case Key.Down:
811 quitMenuYes = !quitMenuYes;
812 needRedraw = true;
813 break;
814 case Key.Home: case Key.PageUp:
815 quitMenuYes = true;
816 needRedraw = true;
817 break;
818 case Key.End: case Key.PageDown:
819 quitMenuYes = false;
820 needRedraw = true;
821 break;
822 case Key.Enter:
823 inQuitMenu = false;
824 needRedraw = true;
825 if (quitMenuYes) doQuit = true;
826 break;
827 default:
829 return true;
832 int arrowDir = 0;
834 bool readerKey (KeyEvent event) {
835 if (atomicLoad(formatWorks) != 0) return false;
836 if (!event.pressed) {
837 switch (event.key) {
838 case Key.Up: arrowDir = 0; return true;
839 case Key.Down: arrowDir = 0; return true;
840 default:
842 return false;
844 switch (event.key) {
845 case Key.Space:
846 if (event.modifierState&ModifierState.shift) {
847 //goto case Key.PageUp;
848 scrollBy(-textHeight/3*2);
849 } else {
850 //goto case Key.PageDown;
851 scrollBy(textHeight/3*2);
853 break;
854 case Key.Backspace:
855 popPosition();
856 break;
857 case Key.PageUp:
858 hardScrollBy(toMove); toMove = 0;
859 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
860 break;
861 case Key.PageDown:
862 hardScrollBy(toMove); toMove = 0;
863 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
864 break;
865 case Key.Up:
866 //scrollBy(-8);
867 arrowDir = -1;
868 break;
869 case Key.Down:
870 //scrollBy(8);
871 arrowDir = 1;
872 break;
873 case Key.H:
874 goHome();
875 break;
876 default:
878 return true;
881 bool controlKey (KeyEvent event) {
882 if (!event.pressed) return false;
883 switch (event.key) {
884 case Key.Escape:
885 if (showShip) { showShip = false; needRedraw = true; return true; }
886 if (inQuitMenu) { inQuitMenu = false; needRedraw = true; return true; }
887 if (inSectionMenu) { inSectionMenu = false; needRedraw = true; return true; }
888 inQuitMenu = true;
889 quitMenuYes = true;
890 needRedraw = true;
891 return true;
892 case Key.P: if (event.modifierState == ModifierState.ctrl) { fpsVisible = !fpsVisible; needRedraw = true; return true; } break;
893 case Key.I: if (event.modifierState == ModifierState.ctrl) { interAllowed = !interAllowed; needRedraw = true; return true; } break;
894 case Key.N: if (event.modifierState == ModifierState.ctrl) { sbLeft = !sbLeft; needRedraw = true; return true; } break;
895 case Key.C: if (event.modifierState == ModifierState.ctrl) { addIf(); return true; } break;
896 case Key.E: if (event.modifierState == ModifierState.ctrl) { showShip = !showShip; needRedraw = true; return true; } break;
897 case Key.Q: if (event.modifierState == ModifierState.ctrl) { doQuit = true; return true; } break;
898 case Key.B: if (event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
899 case Key.S:
900 if (!showShip && !inQuitMenu) {
901 inSectionMenu = !inSectionMenu;
902 if (inSectionMenu) curSectionNum = -1; // move to current
903 needRedraw = true;
905 break;
906 default:
908 return showShip;
911 sdwindow.redrawOpenGlScene = delegate () {
912 if (doQuit) return;
913 // timers
914 prevt = curt;
915 curt = MonoTime.currTime;
916 secs = cast(double)((curt-stt).total!"msecs")/1000.0;
917 dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
919 //glClearColor(0, 0, 0, 0);
920 //glClearColor(0.18, 0.18, 0.18, 0);
921 glClearColor(
922 cast(float)((colorBack>>16)&0xff)/255.0f,
923 cast(float)((colorBack>>8)&0xff)/255.0f,
924 cast(float)(colorBack&0xff)/255.0f,
927 glClear(glNVGClearFlags);
929 if (vg !is null) {
930 if (fps !is null) fps.update(dt);
931 vg.beginFrame(GWidth, GHeight, 1);
932 { // draw page
933 drawIfs();
934 vg.beginPath();
935 if (true/*atomicLoad(formatWorks) == 0*/) {
937 int curHeight = (topLine > 0 && topLine < lines.length ? lines[topLine].y : 0)+topYOfs;
938 float sz = cast(float)(GHeight-4)/cast(float)totalHeight;
939 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
940 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
941 vg.bndScrollBar(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, cast(float)curHeight/cast(float)totalHeight, sz);
944 vg.fontFaceId(textFont);
945 vg.textAlign(NVGAlign.Left|NVGAlign.Top);
946 vg.fontSize(fSize);
947 vg.fillColor(u2c(colorText));
948 int y = -topYOfs, drawY = (GHeight-textHeight)/2, linenum = topLine;
949 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
950 drawY -= topYOfs;
951 while (y < textHeight && linenum < lines.length) {
952 if (newYFade) {
953 //int curY = lines[linenum].y;
954 if (/*newY >= curY && newY < curY+lines[linenum].height*/newYLine == linenum) {
955 vg.fillColor(nvgLerpRGBA(u2c(colorText), u2c(colorTextHi), newYAlpha));
956 //vg.globalAlpha(newYAlpha);
957 } else {
958 vg.fillColor(u2c(colorText));
959 //vg.globalAlpha(1);
962 if (auto inc = lines.ptr[linenum].padTop) { y += inc; drawY += inc; }
963 int tx;
964 int ty = drawY+lines[linenum].textHeight/2-1;
965 if (lines.ptr[linenum].right) {
966 vg.textAlign(NVGAlign.Right|NVGAlign.Middle);
967 tx = GWidth-(sbLeft ? 2+BND_SCROLLBAR_WIDTH : 0)-2;
968 vg.fontFaceId(epiFont);
969 } else if (lines.ptr[linenum].center) {
970 vg.textAlign(NVGAlign.Center|NVGAlign.Middle);
971 tx = GWidth/2;
972 } else {
973 vg.textAlign(NVGAlign.Left|NVGAlign.Middle);
974 tx = (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4);
976 vg.text(tx, ty, lines[linenum].line);
977 if (lines.ptr[linenum].right) vg.fontFaceId(textFont);
978 if (auto inc = lines[linenum].textHeight+lines.ptr[linenum].padBottom) { y += inc; drawY += inc; }
979 ++linenum;
981 vg.fill();
984 // dim text
985 if (!showShip) {
986 if ((colorDim&0xff000000) != 0) {
987 vg.scissor(0, 0, GWidth, GHeight);
988 vg.beginPath();
989 vg.fillColor(u2ca(colorDim));
990 vg.rect(0, 0, GWidth, GHeight);
991 vg.fill();
994 // dim more if menu is active
995 if (inSectionMenu || inQuitMenu || showShip || atomicLoad(formatWorks) != 0) {
996 vg.scissor(0, 0, GWidth, GHeight);
997 vg.beginPath();
998 //vg.globalAlpha(0.5);
999 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || atomicLoad(formatWorks) != 0 ? 127 : 64)));
1000 vg.rect(0, 0, GWidth, GHeight);
1001 vg.fill();
1003 if (atomicLoad(formatWorks) == 0) {
1004 if (inSectionMenu) {
1005 vg.scissor(0, 0, GWidth, GHeight);
1006 drawSectionMenu();
1008 if (inQuitMenu) {
1009 vg.scissor(0, 0, GWidth, GHeight);
1010 drawQuitMenu();
1013 if (showShip) {
1014 drawElite(model, skin, rotx, rotz, 0, 0, GWidth, GHeight);
1015 vg.blitElite();
1017 if (fps !is null && fpsVisible) {
1018 vg.scissor(0, 0, GWidth, GHeight);
1019 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
1021 vg.endFrame();
1023 needRedraw = (fps !is null && fpsVisible);
1026 MonoTime nextIfTime = MonoTime.currTime;
1028 void processThreads () {
1029 ReformatWorkDone wd;
1030 for (;;) {
1031 bool workDone = false;
1032 auto res = receiveTimeout(Duration.zero,
1033 (QuitWork w) {
1034 atomicStore(formatWorks, -1);
1036 (ReformatWorkDone w) {
1037 wd = w;
1038 workDone = true;
1041 if (!res) { assert(!workDone); break; }
1042 if (workDone) { workDone = false; formatComplete(wd); }
1046 sdwindow.eventLoop(1000/57,
1047 delegate () {
1048 processThreads();
1049 if (sdwindow.closed) return;
1050 if (doQuit) { closeWindow(); return; }
1051 enum Delta = 92;
1052 if (atomicLoad(formatWorks) == 0) {
1053 if (toMove < 0) {
1054 int sc = -toMove;
1055 if (sc > Delta) sc = Delta;
1056 hardScrollBy(-sc);
1057 toMove += sc;
1058 nextFadeTime = MonoTime.currTime+500.msecs;
1059 } else if (toMove > 0) {
1060 int sc = toMove;
1061 if (sc > Delta) sc = Delta;
1062 hardScrollBy(sc);
1063 toMove -= sc;
1064 nextFadeTime = MonoTime.currTime+500.msecs;
1065 } else if (arrowDir) {
1066 hardScrollBy(arrowDir*6);
1069 if (newYFade) {
1070 auto ct = MonoTime.currTime;
1071 if (atomicLoad(formatWorks) == 0) {
1072 if (ct >= nextFadeTime) {
1073 if ((newYAlpha -= 0.1) <= 0) {
1074 newYFade = false;
1075 } else {
1076 nextFadeTime = ct+15.msecs;
1078 needRedraw = true;
1083 auto ct = MonoTime.currTime;
1084 if (ct >= nextIfTime) {
1085 import std.random : uniform;
1086 if (uniform!"[]"(0, 100) >= 50) addIf();
1087 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
1090 processIfs();
1091 if (showShip) {
1092 __gshared bool skip;
1093 skip = !skip;
1094 if (skip) rotx = (rotx+1)%120;
1095 rotz = (rotz+1)%120;
1096 needRedraw = true;
1098 if (needRedraw) sdwindow.redrawOpenGlSceneNow();
1099 doSaveState();
1101 delegate (KeyEvent event) {
1102 if (sdwindow.closed) return;
1103 if (event.key == Key.PadEnter) event.key = Key.Enter;
1104 if ((event.modifierState&ModifierState.numLock) == 0) {
1105 switch (event.key) {
1106 case Key.Pad0: event.key = Key.Insert; break;
1107 case Key.PadDot: event.key = Key.Delete; break;
1108 case Key.Pad1: event.key = Key.End; break;
1109 case Key.Pad2: event.key = Key.Down; break;
1110 case Key.Pad3: event.key = Key.PageDown; break;
1111 case Key.Pad4: event.key = Key.Left; break;
1112 case Key.Pad6: event.key = Key.Right; break;
1113 case Key.Pad7: event.key = Key.Home; break;
1114 case Key.Pad8: event.key = Key.Up; break;
1115 case Key.Pad9: event.key = Key.PageUp; break;
1116 //case Key.PadEnter: event.key = Key.Enter; break;
1117 default:
1120 if (controlKey(event)) return;
1121 if (quitMenuKey(event)) return;
1122 if (sectionMenuKey(event)) return;
1123 if (readerKey(event)) return;
1125 delegate (MouseEvent event) {
1127 delegate (dchar ch) {
1128 //if (ch == 'q') { doQuit = true; return; }
1131 closeWindow();
1133 childTid.send(QuitWork());
1134 while (atomicLoad(formatWorks) >= 0) processThreads();
1138 // ////////////////////////////////////////////////////////////////////////// //
1139 void main (string[] args) {
1140 import std.path;
1141 RcDir = RcDir.expandTilde.absolutePath;
1142 //writeln(RcDir);
1144 if (args.length == 1) {
1145 try {
1146 string lnn;
1147 foreach (string line; File(buildPath(RcDir, ".lastfile")).byLineCopy) {
1148 if (!line.isComment) lnn = line;
1150 if (lnn.length) args ~= lnn;
1151 } catch (Exception) {}
1153 if (args.length == 1) assert(0, "no filename");
1155 loadShip("ferdelance");
1157 readConfig();
1159 auto fname = args[1].expandTilde.absolutePath;
1160 writeln("loading '", fname, "'...");
1161 auto stt = MonoTime.currTime;
1162 auto book = new BookText(fname);
1163 writeln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds");
1165 string[] lines;
1166 string[] comments;
1167 try {
1168 foreach (string line; File(buildPath(RcDir, ".lastfile")).byLineCopy) {
1169 if (line != fname) {
1170 lines ~= line;
1171 } else {
1172 while (lines.length && lines[$-1].isComment) {
1173 comments ~= lines[$-1];
1174 lines = lines[0..$-1];
1178 } catch (Exception) {}
1179 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
1180 auto fo = File(buildPath(RcDir, ".lastfile"), "w");
1181 foreach (string s; lines) fo.writeln(s);
1182 foreach_reverse (string s; comments) fo.writeln(s);
1183 fo.writeln(fname);
1185 stateFileName = buildPath(RcDir, fname.baseName~".rc");
1186 run(book);