use text formatter method to get marked text
[xreader.git] / xreaderui.d
bloba6c71a828d6bcbcc1a480822eae9ff0762ad3697
1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
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 xreaderui;
19 import arsd.simpledisplay;
20 import arsd.image;
22 import iv.nanovega;
23 import iv.nanovega.blendish;
24 import iv.strex;
25 import iv.utfutil;
26 import iv.vfs;
27 import iv.vfs.io;
29 import xreadercfg;
32 // ////////////////////////////////////////////////////////////////////////// //
33 void loadFonts (NVGContext vg) {
34 void loadFont (ref int fid, string name, string path) {
35 fid = vg.createFont(name, path);
36 if (fid < 0) throw new Exception("can't load font '"~name~"' from '"~path~"'");
37 //writeln("id=", fid, "; name=<", name, ">; path=", path);
40 loadFont(textFont, "text", textFontName);
41 loadFont(textiFont, "texti", textiFontName);
42 loadFont(textbFont, "textb", textbFontName);
43 loadFont(textzFont, "textz", textzFontName);
45 loadFont(monoFont, "mono", monoFontName);
46 loadFont(monoiFont, "monoi", monoiFontName);
47 loadFont(monobFont, "monob", monobFontName);
48 loadFont(monozFont, "monoz", monozFontName);
50 loadFont(uiFont, "ui", uiFontName);
52 loadFont(galmapFont, "galmap", galmapFontName);
54 bndSetFont(uiFont);
58 // ////////////////////////////////////////////////////////////////////////// //
59 // cursor (hi, Death Track!)
60 private enum curWidth = 17;
61 private enum curHeight = 23;
62 private static immutable ubyte[curWidth*curHeight] curImg = [
63 0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
64 0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,
65 1,0,3,2,2,0,0,0,0,0,0,0,0,0,0,0,0,
66 1,1,3,3,2,2,0,0,0,0,0,0,0,0,0,0,0,
67 1,1,3,3,4,2,2,0,0,0,0,0,0,0,0,0,0,
68 1,1,3,3,4,4,2,2,0,0,0,0,0,0,0,0,0,
69 1,1,3,3,4,4,4,2,2,0,0,0,0,0,0,0,0,
70 1,1,3,3,4,4,4,4,2,2,0,0,0,0,0,0,0,
71 1,1,3,3,4,4,4,5,6,2,2,0,0,0,0,0,0,
72 1,1,3,3,4,4,5,6,7,5,2,2,0,0,0,0,0,
73 1,1,3,3,4,5,6,7,5,4,5,2,2,0,0,0,0,
74 1,1,3,3,5,6,7,5,4,5,6,7,2,2,0,0,0,
75 1,1,3,3,6,7,5,4,5,6,7,7,7,2,2,0,0,
76 1,1,3,3,7,5,4,5,6,7,7,7,7,7,2,2,0,
77 1,1,3,3,5,4,5,6,8,8,8,8,8,8,8,8,2,
78 1,1,3,3,4,5,6,3,8,8,8,8,8,8,8,8,8,
79 1,1,3,3,5,6,3,3,1,1,1,1,1,1,1,0,0,
80 1,1,3,3,6,3,3,1,1,1,1,1,1,1,1,0,0,
81 1,1,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,
82 1,1,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,
83 1,1,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,
84 1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
85 1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
87 struct CC { ubyte r, g, b, a; }
88 private static immutable CC[9] curPal = [
89 CC( 0, 0, 0, 0),
90 CC( 0, 0, 0,127),
91 CC( 85,255,255,255),
92 CC( 85, 85,255,255),
93 CC(255, 85, 85,255),
94 CC(170, 0,170,255),
95 CC( 85, 85, 85,255),
96 CC( 0, 0, 0,255),
97 CC( 0, 0,170,255),
101 NVGImage createCursorImage (NVGContext vg, bool white=false) {
102 auto img = new ubyte[](curWidth*curHeight*4);
103 scope(exit) img.destroy;
104 const(ubyte)* s = curImg.ptr;
105 auto d = img.ptr;
106 foreach (immutable _; 0..curWidth*curHeight) {
107 CC c = curPal.ptr[*s++];
108 if (white) {
109 if (c.a == 255) {
110 //c = CC(255, 127, 0, 255);
111 auto hsl = nvgRGB(c.r, c.g, c.b).asHSL;
112 //hsl.l *= 1.25; if (hsl.l > 0.8) hsl.l = 0.8;
113 //hsl.l *= 1.25; if (hsl.l > 1) hsl.l = 1;
114 //hsl.h *= 2.25; if (hsl.h > 1) hsl.h = 1;
115 hsl.h *= 0.1;
116 auto cc = hsl.asColor.asUint;
117 *d++ = cc&0xff; cc >>= 8;
118 *d++ = cc&0xff; cc >>= 8;
119 *d++ = cc&0xff; cc >>= 8;
120 *d++ = cc&0xff; cc >>= 8;
121 } else {
122 *d++ = c.r;
123 *d++ = c.g;
124 *d++ = c.b;
125 *d++ = c.a;
127 } else {
128 *d++ = c.r;
129 *d++ = c.g;
130 *d++ = c.b;
131 *d++ = c.a;
134 return vg.createImageRGBA(curWidth, curHeight, img[]);
138 // ////////////////////////////////////////////////////////////////////////// //
139 final class PopupMenu {
140 //enum LeftX = 10;
141 //enum TopY = 10;
142 enum Padding = 8;
144 static struct SecInfo {
145 int w, h;
146 dstring s;
147 string skoi;
149 void buildKoi () nothrow {
150 skoi = null;
151 foreach (dchar dc; s) skoi ~= uni2koi(dc);
154 private static bool xmatch (const(char)[] s, const(char)[] pat) nothrow @trusted @nogc {
155 if (s.length == 0) return (pat.length == 0);
156 while (s.length && pat.length) {
157 // is pattern char space?
158 if (pat[0] <= ' ') {
159 if (s[0] > ' ') return false;
160 pat = pat.xstrip;
161 s = s.xstrip;
162 continue;
164 // pattern char is not space
165 if (koi8lower(s[0]) != koi8lower(pat[0])) return false;
166 s = s[1..$];
167 pat = pat[1..$];
169 return (pat.length == 0);
172 bool filterMatch (const(char)[] flt) const nothrow @nogc {
173 if (flt.length == 0) return true;
174 //if (skoi.length < flt) return false;
175 foreach (immutable dpos; 0..skoi.length-1) {
176 if (xmatch(skoi[dpos..$], flt)) return true;
178 return false;
182 static struct VisItem {
183 int idx; // item index in `items`
186 bool hasTitle;
187 SecInfo menuTitle;
188 private SecInfo[] items;
189 private VisItem[] visitems;
190 private int curItemIdx = 0;
191 private int topItemIdx = 0;
192 private int hoverItem = -1;
193 NVGContext vg;
194 int maxW; // maximum text width
195 bool needRedraw;
196 int leftX, topY;
197 bool moving;
199 private char[] filter; // WARNING! it is unsafe to slice this!
201 bool allowFiltering = false;
203 void textSize (dstring s, out int tw, out int th) {
204 auto w = vg.bndLabelWidth(-1, s)+4;
205 //{ import core.stdc.stdio : printf; printf("* 03: w=%g\n", cast(double)w); }
206 if (w > GWidth-Padding*4) w = GWidth-Padding*4;
207 //{ import core.stdc.stdio : printf; printf("* 04: w=%g\n", cast(double)w); }
208 auto h = vg.bndLabelHeight(-1, s, w);
209 if (h < BND_WIDGET_HEIGHT) h = BND_WIDGET_HEIGHT;
210 tw = cast(int)w;
211 th = cast(int)h;
214 this (NVGContext avg, dstring atitle, scope dstring[] delegate () buildSections) {
215 assert(avg !is null);
216 //writeln("fw=", avg.width, "; pr=", avg.devicePixelRatio);
217 bool doEndFrame = !avg.inFrame;
218 if (doEndFrame) avg.beginFrame(1024, 768);
219 scope(exit) if (doEndFrame) avg.cancelFrame();
220 vg = avg;
221 vg.fontFaceId = uiFont;
222 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
223 vg.fontSize = fsizeUI;
224 maxW = 0;
225 if (atitle !is null) {
226 hasTitle = true;
227 items.length += 1;
228 items[0].s = atitle;
229 textSize(atitle, items[0].w, items[0].h);
230 maxW = items[0].w;
232 if (buildSections !is null) {
233 auto list = buildSections();
234 auto cpos = items.length;
235 items.length += list.length;
236 foreach (dstring s; list) {
237 items[cpos].s = s;
238 items[cpos].buildKoi();
239 textSize(s, items[cpos].w, items[cpos].h);
240 if (maxW < items[cpos].w) maxW = items[cpos].w;
241 ++cpos;
244 if (hasTitle) {
245 menuTitle = items[0];
246 items = items[1..$];
248 //writeln("maxW=", maxW);
249 leftX = (GWidth-maxW-Padding*4)/2;
250 topY = Padding;
251 int ey = calcBotY;
252 //topY = (GHeight-(ey-topY))/2;
253 topY = Padding;
254 refilter();
255 needRedraw = true;
258 private ref SecInfo itAt (int idx) nothrow @nogc {
259 if (idx < 0 || idx >= visitems.length) assert(0, "ui internal error");
260 return items[visitems[idx].idx];
263 @property bool isCurValid () const pure nothrow @safe @nogc { pragma(inline, true); return (curItemIdx >= 0 && curItemIdx < visitems.length); }
265 @property int itemIndex () const pure nothrow @safe @nogc { pragma(inline, true); return (curItemIdx >= 0 && curItemIdx < visitems.length ? visitems[curItemIdx].idx : -1); }
267 @property void itemIndex (int v) nothrow @trusted @nogc {
268 if (items.length == 0) curItemIdx = 0;
269 else if (v < 0) v = 0;
270 else if (v >= items.length) v = cast(int)(items.length-1);
271 foreach (immutable vidx, const ref vi; visitems) {
272 if (vi.idx == v) { curItemIdx = cast(int)vidx; return; }
274 //curItemIdx = 0;
277 void refilter () {
278 visitems.length = 0;
279 visitems.assumeSafeAppend;
280 foreach (immutable iidx, const ref it; items) {
281 if (it.filterMatch(filter)) {
282 visitems ~= VisItem(cast(int)iidx);
287 void removeItem (int idx) {
288 if (idx < 0 || idx >= items.length) return;
289 int oidx = itemIndex;
290 foreach (immutable c; idx+1..items.length) items[c-1] = items[c];
291 items.length -= 1;
292 items.assumeSafeAppend;
293 if (oidx >= items.length) oidx = cast(int)(items.length-1);
294 if (oidx < 0) oidx = 0;
295 refilter();
296 itemIndex = oidx;
299 int lastFullVisible () {
300 if (visitems.length == 0) return 0;
301 int res = topItemIdx;
302 int y = topY+Padding+menuTitle.h;
303 int idx = topItemIdx;
304 if (idx < 0) idx = 0;
305 int ey = calcBotY-Padding;
306 while (idx < visitems.length) {
307 y += itAt(idx).h;
308 if (y > ey) return res;
309 res = idx;
310 ++idx;
312 return cast(int)visitems.length-1;
315 int calcBotY () {
316 int y = topY+Padding*2+menuTitle.h;
317 foreach (const ref sc; items) {
318 int ny = y+sc.h;
319 if (ny > GHeight-Padding*2) return y;
320 y = ny;
322 return y;
325 void normPage () {
326 if (curItemIdx < 0) curItemIdx = 0;
327 if (curItemIdx >= visitems.length) curItemIdx = cast(int)visitems.length-1;
328 if (visitems.length) {
329 // make current item visible
330 if (curItemIdx >= 0 && curItemIdx < visitems.length) {
331 if (curItemIdx < topItemIdx) {
332 topItemIdx = curItemIdx;
333 } else {
334 //FIXME: make this faster!
335 //conwriteln("top=", topItemIdx, "; cur=", curItemIdx, "; lv=", lastFullVisible);
336 while (lastFullVisible < curItemIdx) {
337 if (topItemIdx >= visitems.length-1) break;
338 ++topItemIdx;
343 while (topItemIdx > 0) {
344 int lv = lastFullVisible;
345 --topItemIdx;
346 int lv1 = lastFullVisible;
347 if (lv1 != lv) { ++topItemIdx; break; }
351 void draw () {
352 vg.save();
353 scope(exit) vg.restore();
354 if (moving) vg.globalAlpha(0.8);
355 vg.fontFaceId(uiFont);
356 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
357 vg.fontSize(fsizeUI);
358 normPage();
359 int ey = calcBotY;
360 //writeln("leftX=", leftX, "; topY=", topY, "; w=", maxW+Padding*2, "; h=", ey-topY);
361 vg.bndMenuBackground(leftX, topY, maxW+Padding*2, ey-topY, BND_CORNER_NONE);
362 int y = topY+Padding;
363 vg.scissor(leftX+Padding, y, maxW, ey-Padding);
364 if (hasTitle && filter.length == 0) {
365 vg.bndMenuLabel(leftX+Padding, y, maxW, menuTitle.h, -1, menuTitle.s);
366 y += menuTitle.h;
368 if (filter.length != 0 && hasTitle) { //FIXME
369 char[256] buf = void;
370 char[4] ub = void;
371 int bufpos = 0;
372 foreach (char fch; filter) {
373 auto len = utf8Encode(ub[], koi2uni(fch));
374 if (len < 1) continue;
375 if (bufpos+len > buf.length) break;
376 buf[bufpos..bufpos+len] = ub[0..len];
377 bufpos += len;
379 vg.bndMenuLabel(leftX+Padding, y, maxW, menuTitle.h, -1, buf[0..bufpos]);
380 y += menuTitle.h;
382 int idx = topItemIdx;
383 if (idx < 0) idx = 0;
384 ey -= Padding;
385 while (idx < visitems.length) {
386 vg.bndMenuItem(leftX+Padding, y, maxW, itAt(idx).h,
387 (curItemIdx == idx ? BND_ACTIVE : hoverItem == idx ? BND_HOVER : BND_DEFAULT),
388 -1, itAt(idx).s);
389 y += itAt(idx).h;
390 if (y >= ey) break;
391 ++idx;
393 needRedraw = false;
396 enum int Close = -666;
397 enum int Eaten = -2;
398 enum int NotMine = -1;
400 int onKey (KeyEvent event) {
401 if (!event.pressed) return NotMine;
402 switch (event.key) {
403 case Key.Escape:
404 return Close;
405 case Key.Up:
406 if (curItemIdx > 0) {
407 needRedraw = true;
408 --curItemIdx;
410 return Eaten;
411 case Key.Down:
412 if (curItemIdx+1 < visitems.length) {
413 needRedraw = true;
414 ++curItemIdx;
416 return Eaten;
417 case Key.PageUp:
418 if (visitems.length) {
419 if (curItemIdx == topItemIdx) {
420 while (topItemIdx > 0 && lastFullVisible != curItemIdx) --topItemIdx;
422 needRedraw = true;
423 curItemIdx = topItemIdx;
425 return Eaten;
426 case Key.PageDown:
427 if (visitems.length) {
428 if (curItemIdx == lastFullVisible) {
429 while (lastFullVisible < visitems.length && topItemIdx != curItemIdx) ++topItemIdx;
431 needRedraw = true;
432 curItemIdx = lastFullVisible;
434 return Eaten;
435 case Key.Home:
436 curItemIdx = 0;
437 needRedraw = true;
438 return Eaten;
439 case Key.End:
440 if (visitems.length) {
441 curItemIdx = cast(int)visitems.length-1;
442 needRedraw = true;
444 return Eaten;
445 case Key.Enter:
446 return (isCurValid ? itemIndex : Close);
447 default:
449 // clear filter
450 if (allowFiltering && event == "C-Y") {
451 if (filter.length) {
452 filter.length = 0;
453 filter.assumeSafeAppend;
454 auto cidx = itemIndex;
455 refilter();
456 itemIndex = cidx;
457 needRedraw = true;
458 return Eaten;
461 if (allowFiltering && filter.length && event == "Backspace") {
462 filter.length -= 1;
463 filter.assumeSafeAppend;
464 auto cidx = itemIndex;
465 refilter();
466 itemIndex = cidx;
467 needRedraw = true;
468 return Eaten;
470 return NotMine;
473 int onChar (dchar dch) {
474 if (!allowFiltering || dch < ' ') return NotMine;
475 char ch = uni2koi(dch);
476 if (ch < ' ' || ch == 127) return NotMine;
477 filter ~= ch;
478 auto cidx = itemIndex;
479 refilter();
480 itemIndex = cidx;
481 needRedraw = true;
482 return Eaten;
485 int onMouse (MouseEvent event) {
486 int atItem = -1;
487 bool inside = false;
488 if (event.x >= leftX+Padding && event.x < leftX+Padding+maxW && event.y >= topY+Padding) {
489 normPage();
490 int ey = calcBotY-Padding;
491 if (event.y < ey) {
492 inside = true;
493 int y = topY+Padding;
494 if (hasTitle) y += menuTitle.h;
495 int idx = topItemIdx;
496 if (idx < 0) idx = 0;
497 while (idx < visitems.length) {
498 if (event.y >= y && event.y < y+itAt(idx).h) { atItem = idx; break; }
499 y += itAt(idx).h;
500 if (y >= ey) break;
501 ++idx;
505 if (hoverItem != atItem) {
506 hoverItem = atItem;
507 needRedraw = true;
509 switch (event.type) {
510 case MouseEventType.motion:
511 if (moving) {
512 leftX += event.dx;
513 topY += event.dy;
514 needRedraw = true;
516 break;
517 case MouseEventType.buttonPressed:
518 if (event.button == MouseButton.right) return Close;
519 if (event.button == MouseButton.left) {
520 //if (hoverItem >= 0 && hoverItem < visitems.length) return hoverItem;
521 if (hoverItem >= 0 && hoverItem < visitems.length) {
522 curItemIdx = hoverItem;
523 } else {
524 if (inside) {
525 moving = true;
526 needRedraw = true;
530 if (event.button == MouseButton.wheelUp) {
531 //if (topItemIdx > 0) { --topItemIdx; needRedraw = true; }
532 if (curItemIdx > 0) { --curItemIdx; needRedraw = true; }
534 if (event.button == MouseButton.wheelDown) {
535 //if (topItemIdx < visitems.length) { ++topItemIdx; needRedraw = true; }
536 if (curItemIdx < visitems.length) { ++curItemIdx; needRedraw = true; }
538 break;
539 case MouseEventType.buttonReleased:
540 if (event.button == MouseButton.left) {
541 //if (curItemIdx >= 0 && curItemIdx < visitems.length) return curItemIdx;
542 if (curItemIdx == hoverItem && curItemIdx >= 0 && curItemIdx < visitems.length && isCurValid) return itemIndex;
543 moving = false;
544 needRedraw = true;
546 break;
547 default:
549 return NotMine;