updated for new NanoVega
[xreader.git] / xreaderui.d
blob4db667f5356ff941941c331dc73fd20e693364d1
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~"'");
39 loadFont(textFont, "text", textFontName);
40 loadFont(textiFont, "texti", textiFontName);
41 loadFont(textbFont, "textb", textbFontName);
42 loadFont(textzFont, "textz", textzFontName);
44 loadFont(monoFont, "mono", monoFontName);
45 loadFont(monoiFont, "monoi", monoiFontName);
46 loadFont(monobFont, "monob", monobFontName);
47 loadFont(monozFont, "monoz", monozFontName);
49 loadFont(uiFont, "ui", uiFontName);
51 loadFont(galmapFont, "galmap", galmapFontName);
53 bndSetFont(uiFont);
57 // ////////////////////////////////////////////////////////////////////////// //
58 // cursor (hi, Death Track!)
59 private enum curWidth = 17;
60 private enum curHeight = 23;
61 private static immutable ubyte[curWidth*curHeight] curImg = [
62 0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
63 0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,
64 1,0,3,2,2,0,0,0,0,0,0,0,0,0,0,0,0,
65 1,1,3,3,2,2,0,0,0,0,0,0,0,0,0,0,0,
66 1,1,3,3,4,2,2,0,0,0,0,0,0,0,0,0,0,
67 1,1,3,3,4,4,2,2,0,0,0,0,0,0,0,0,0,
68 1,1,3,3,4,4,4,2,2,0,0,0,0,0,0,0,0,
69 1,1,3,3,4,4,4,4,2,2,0,0,0,0,0,0,0,
70 1,1,3,3,4,4,4,5,6,2,2,0,0,0,0,0,0,
71 1,1,3,3,4,4,5,6,7,5,2,2,0,0,0,0,0,
72 1,1,3,3,4,5,6,7,5,4,5,2,2,0,0,0,0,
73 1,1,3,3,5,6,7,5,4,5,6,7,2,2,0,0,0,
74 1,1,3,3,6,7,5,4,5,6,7,7,7,2,2,0,0,
75 1,1,3,3,7,5,4,5,6,7,7,7,7,7,2,2,0,
76 1,1,3,3,5,4,5,6,8,8,8,8,8,8,8,8,2,
77 1,1,3,3,4,5,6,3,8,8,8,8,8,8,8,8,8,
78 1,1,3,3,5,6,3,3,1,1,1,1,1,1,1,0,0,
79 1,1,3,3,6,3,3,1,1,1,1,1,1,1,1,0,0,
80 1,1,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,
81 1,1,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,
82 1,1,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,
83 1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
84 1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
86 struct CC { ubyte r, g, b, a; }
87 private static immutable CC[9] curPal = [
88 CC( 0, 0, 0, 0),
89 CC( 0, 0, 0,127),
90 CC( 85,255,255,255),
91 CC( 85, 85,255,255),
92 CC(255, 85, 85,255),
93 CC(170, 0,170,255),
94 CC( 85, 85, 85,255),
95 CC( 0, 0, 0,255),
96 CC( 0, 0,170,255),
100 NVGImage createCursorImage (NVGContext vg, bool white=false) {
101 auto img = new ubyte[](curWidth*curHeight*4);
102 scope(exit) img.destroy;
103 const(ubyte)* s = curImg.ptr;
104 auto d = img.ptr;
105 foreach (immutable _; 0..curWidth*curHeight) {
106 CC c = curPal.ptr[*s++];
107 if (white) {
108 if (c.a == 255) {
109 //c = CC(255, 127, 0, 255);
110 auto hsl = nvgRGB(c.r, c.g, c.b).asHSL;
111 //hsl.l *= 1.25; if (hsl.l > 0.8) hsl.l = 0.8;
112 //hsl.l *= 1.25; if (hsl.l > 1) hsl.l = 1;
113 //hsl.h *= 2.25; if (hsl.h > 1) hsl.h = 1;
114 hsl.h *= 0.1;
115 auto cc = hsl.asColor.asUint;
116 *d++ = cc&0xff; cc >>= 8;
117 *d++ = cc&0xff; cc >>= 8;
118 *d++ = cc&0xff; cc >>= 8;
119 *d++ = cc&0xff; cc >>= 8;
120 } else {
121 *d++ = c.r;
122 *d++ = c.g;
123 *d++ = c.b;
124 *d++ = c.a;
126 } else {
127 *d++ = c.r;
128 *d++ = c.g;
129 *d++ = c.b;
130 *d++ = c.a;
133 return vg.createImageRGBA(curWidth, curHeight, img[]);
137 // ////////////////////////////////////////////////////////////////////////// //
138 final class PopupMenu {
139 //enum LeftX = 10;
140 //enum TopY = 10;
141 enum Padding = 8;
143 static struct SecInfo {
144 int w, h;
145 dstring s;
146 string skoi;
148 void buildKoi () nothrow {
149 skoi = null;
150 foreach (dchar dc; s) skoi ~= uni2koi(dc);
153 private static bool xmatch (const(char)[] s, const(char)[] pat) nothrow @trusted @nogc {
154 if (s.length == 0) return (pat.length == 0);
155 while (s.length && pat.length) {
156 // is pattern char space?
157 if (pat[0] <= ' ') {
158 if (s[0] > ' ') return false;
159 pat = pat.xstrip;
160 s = s.xstrip;
161 continue;
163 // pattern char is not space
164 if (koi8lower(s[0]) != koi8lower(pat[0])) return false;
165 s = s[1..$];
166 pat = pat[1..$];
168 return (pat.length == 0);
171 bool filterMatch (const(char)[] flt) const nothrow @nogc {
172 if (flt.length == 0) return true;
173 //if (skoi.length < flt) return false;
174 foreach (immutable dpos; 0..skoi.length-1) {
175 if (xmatch(skoi[dpos..$], flt)) return true;
177 return false;
181 static struct VisItem {
182 int idx; // item index in `items`
185 bool hasTitle;
186 SecInfo menuTitle;
187 private SecInfo[] items;
188 private VisItem[] visitems;
189 private int curItemIdx = 0;
190 private int topItemIdx = 0;
191 private int hoverItem = -1;
192 NVGContext vg;
193 int maxW; // maximum text width
194 bool needRedraw;
195 int leftX, topY;
196 bool moving;
198 private char[] filter; // WARNING! it is unsafe to slice this!
200 bool allowFiltering = false;
202 void textSize (dstring s, out int tw, out int th) {
203 auto w = vg.bndLabelWidth(-1, s)+4;
204 if (w > GWidth-Padding*4) w = GWidth-Padding*4;
205 auto h = vg.bndLabelHeight(-1, s, w);
206 if (h < BND_WIDGET_HEIGHT) h = BND_WIDGET_HEIGHT;
207 tw = cast(int)w;
208 th = cast(int)h;
211 this (NVGContext avg, dstring atitle, scope dstring[] delegate () buildSections) {
212 assert(avg !is null);
213 vg = avg;
214 vg.fontFaceId(uiFont);
215 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
216 vg.fontSize(fsizeUI);
217 maxW = 0;
218 if (atitle !is null) {
219 hasTitle = true;
220 items.length += 1;
221 items[0].s = atitle;
222 textSize(atitle, items[0].w, items[0].h);
223 maxW = items[0].w;
225 if (buildSections !is null) {
226 auto list = buildSections();
227 auto cpos = items.length;
228 items.length += list.length;
229 foreach (dstring s; list) {
230 items[cpos].s = s;
231 items[cpos].buildKoi();
232 textSize(s, items[cpos].w, items[cpos].h);
233 if (maxW < items[cpos].w) maxW = items[cpos].w;
234 ++cpos;
237 if (hasTitle) {
238 menuTitle = items[0];
239 items = items[1..$];
241 leftX = (GWidth-maxW-Padding*4)/2;
242 topY = Padding;
243 int ey = calcBotY;
244 //topY = (GHeight-(ey-topY))/2;
245 topY = Padding;
246 refilter();
247 needRedraw = true;
250 private ref SecInfo itAt (int idx) nothrow @nogc {
251 if (idx < 0 || idx >= visitems.length) assert(0, "ui internal error");
252 return items[visitems[idx].idx];
255 @property bool isCurValid () const pure nothrow @safe @nogc { pragma(inline, true); return (curItemIdx >= 0 && curItemIdx < visitems.length); }
257 @property int itemIndex () const pure nothrow @safe @nogc { pragma(inline, true); return (curItemIdx >= 0 && curItemIdx < visitems.length ? visitems[curItemIdx].idx : -1); }
259 @property void itemIndex (int v) nothrow @trusted @nogc {
260 if (items.length == 0) curItemIdx = 0;
261 else if (v < 0) v = 0;
262 else if (v >= items.length) v = cast(int)(items.length-1);
263 foreach (immutable vidx, const ref vi; visitems) {
264 if (vi.idx == v) { curItemIdx = cast(int)vidx; return; }
266 //curItemIdx = 0;
269 void refilter () {
270 visitems.length = 0;
271 visitems.assumeSafeAppend;
272 foreach (immutable iidx, const ref it; items) {
273 if (it.filterMatch(filter)) {
274 visitems ~= VisItem(cast(int)iidx);
279 void removeItem (int idx) {
280 if (idx < 0 || idx >= items.length) return;
281 int oidx = itemIndex;
282 foreach (immutable c; idx+1..items.length) items[c-1] = items[c];
283 items.length -= 1;
284 items.assumeSafeAppend;
285 if (oidx >= items.length) oidx = cast(int)(items.length-1);
286 if (oidx < 0) oidx = 0;
287 refilter();
288 itemIndex = oidx;
291 int lastFullVisible () {
292 if (visitems.length == 0) return 0;
293 int res = topItemIdx;
294 int y = topY+Padding+menuTitle.h;
295 int idx = topItemIdx;
296 if (idx < 0) idx = 0;
297 int ey = calcBotY-Padding;
298 while (idx < visitems.length) {
299 y += itAt(idx).h;
300 if (y > ey) return res;
301 res = idx;
302 ++idx;
304 return cast(int)visitems.length-1;
307 int calcBotY () {
308 int y = topY+Padding*2+menuTitle.h;
309 foreach (const ref sc; items) {
310 int ny = y+sc.h;
311 if (ny > GHeight-Padding*2) return y;
312 y = ny;
314 return y;
317 void normPage () {
318 if (curItemIdx < 0) curItemIdx = 0;
319 if (curItemIdx >= visitems.length) curItemIdx = cast(int)visitems.length-1;
320 if (visitems.length) {
321 // make current item visible
322 if (curItemIdx >= 0 && curItemIdx < visitems.length) {
323 if (curItemIdx < topItemIdx) {
324 topItemIdx = curItemIdx;
325 } else {
326 //FIXME: make this faster!
327 //conwriteln("top=", topItemIdx, "; cur=", curItemIdx, "; lv=", lastFullVisible);
328 while (lastFullVisible < curItemIdx) {
329 if (topItemIdx >= visitems.length-1) break;
330 ++topItemIdx;
335 while (topItemIdx > 0) {
336 int lv = lastFullVisible;
337 --topItemIdx;
338 int lv1 = lastFullVisible;
339 if (lv1 != lv) { ++topItemIdx; break; }
343 void draw () {
344 vg.save();
345 scope(exit) vg.restore();
346 if (moving) vg.globalAlpha(0.8);
347 vg.fontFaceId(uiFont);
348 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
349 vg.fontSize(fsizeUI);
350 normPage();
351 int ey = calcBotY;
352 vg.bndMenuBackground(leftX, topY, maxW+Padding*2, ey-topY, BND_CORNER_NONE);
353 int y = topY+Padding;
354 vg.scissor(leftX+Padding, y, maxW, ey-Padding);
355 if (hasTitle && filter.length == 0) {
356 vg.bndMenuLabel(leftX+Padding, y, maxW, menuTitle.h, -1, menuTitle.s);
357 y += menuTitle.h;
359 if (filter.length != 0 && hasTitle) { //FIXME
360 char[256] buf = void;
361 char[4] ub = void;
362 int bufpos = 0;
363 foreach (char fch; filter) {
364 auto len = utf8Encode(ub[], koi2uni(fch));
365 if (len < 1) continue;
366 if (bufpos+len > buf.length) break;
367 buf[bufpos..bufpos+len] = ub[0..len];
368 bufpos += len;
370 vg.bndMenuLabel(leftX+Padding, y, maxW, menuTitle.h, -1, buf[0..bufpos]);
371 y += menuTitle.h;
373 int idx = topItemIdx;
374 if (idx < 0) idx = 0;
375 ey -= Padding;
376 while (idx < visitems.length) {
377 vg.bndMenuItem(leftX+Padding, y, maxW, itAt(idx).h,
378 (curItemIdx == idx ? BND_ACTIVE : hoverItem == idx ? BND_HOVER : BND_DEFAULT),
379 -1, itAt(idx).s);
380 y += itAt(idx).h;
381 if (y >= ey) break;
382 ++idx;
384 needRedraw = false;
387 enum int Close = -666;
388 enum int Eaten = -2;
389 enum int NotMine = -1;
391 int onKey (KeyEvent event) {
392 if (!event.pressed) return NotMine;
393 switch (event.key) {
394 case Key.Escape:
395 return Close;
396 case Key.Up:
397 if (curItemIdx > 0) {
398 needRedraw = true;
399 --curItemIdx;
401 return Eaten;
402 case Key.Down:
403 if (curItemIdx+1 < visitems.length) {
404 needRedraw = true;
405 ++curItemIdx;
407 return Eaten;
408 case Key.PageUp:
409 if (visitems.length) {
410 if (curItemIdx == topItemIdx) {
411 while (topItemIdx > 0 && lastFullVisible != curItemIdx) --topItemIdx;
413 needRedraw = true;
414 curItemIdx = topItemIdx;
416 return Eaten;
417 case Key.PageDown:
418 if (visitems.length) {
419 if (curItemIdx == lastFullVisible) {
420 while (lastFullVisible < visitems.length && topItemIdx != curItemIdx) ++topItemIdx;
422 needRedraw = true;
423 curItemIdx = lastFullVisible;
425 return Eaten;
426 case Key.Home:
427 curItemIdx = 0;
428 needRedraw = true;
429 return Eaten;
430 case Key.End:
431 if (visitems.length) {
432 curItemIdx = cast(int)visitems.length-1;
433 needRedraw = true;
435 return Eaten;
436 case Key.Enter:
437 return (isCurValid ? itemIndex : Close);
438 default:
440 // clear filter
441 if (allowFiltering && event == "C-Y") {
442 if (filter.length) {
443 filter.length = 0;
444 filter.assumeSafeAppend;
445 auto cidx = itemIndex;
446 refilter();
447 itemIndex = cidx;
448 needRedraw = true;
449 return Eaten;
452 if (allowFiltering && filter.length && event == "Backspace") {
453 filter.length -= 1;
454 filter.assumeSafeAppend;
455 auto cidx = itemIndex;
456 refilter();
457 itemIndex = cidx;
458 needRedraw = true;
459 return Eaten;
461 return NotMine;
464 int onChar (dchar dch) {
465 if (!allowFiltering || dch < ' ') return NotMine;
466 char ch = uni2koi(dch);
467 if (ch < ' ' || ch == 127) return NotMine;
468 filter ~= ch;
469 auto cidx = itemIndex;
470 refilter();
471 itemIndex = cidx;
472 needRedraw = true;
473 return Eaten;
476 int onMouse (MouseEvent event) {
477 int atItem = -1;
478 bool inside = false;
479 if (event.x >= leftX+Padding && event.x < leftX+Padding+maxW && event.y >= topY+Padding) {
480 normPage();
481 int ey = calcBotY-Padding;
482 if (event.y < ey) {
483 inside = true;
484 int y = topY+Padding;
485 if (hasTitle) y += menuTitle.h;
486 int idx = topItemIdx;
487 if (idx < 0) idx = 0;
488 while (idx < visitems.length) {
489 if (event.y >= y && event.y < y+itAt(idx).h) { atItem = idx; break; }
490 y += itAt(idx).h;
491 if (y >= ey) break;
492 ++idx;
496 if (hoverItem != atItem) {
497 hoverItem = atItem;
498 needRedraw = true;
500 switch (event.type) {
501 case MouseEventType.motion:
502 if (moving) {
503 leftX += event.dx;
504 topY += event.dy;
505 needRedraw = true;
507 break;
508 case MouseEventType.buttonPressed:
509 if (event.button == MouseButton.right) return Close;
510 if (event.button == MouseButton.left) {
511 //if (hoverItem >= 0 && hoverItem < visitems.length) return hoverItem;
512 if (hoverItem >= 0 && hoverItem < visitems.length) {
513 curItemIdx = hoverItem;
514 } else {
515 if (inside) {
516 moving = true;
517 needRedraw = true;
521 if (event.button == MouseButton.wheelUp) {
522 //if (topItemIdx > 0) { --topItemIdx; needRedraw = true; }
523 if (curItemIdx > 0) { --curItemIdx; needRedraw = true; }
525 if (event.button == MouseButton.wheelDown) {
526 //if (topItemIdx < visitems.length) { ++topItemIdx; needRedraw = true; }
527 if (curItemIdx < visitems.length) { ++curItemIdx; needRedraw = true; }
529 break;
530 case MouseEventType.buttonReleased:
531 if (event.button == MouseButton.left) {
532 //if (curItemIdx >= 0 && curItemIdx < visitems.length) return curItemIdx;
533 if (curItemIdx == hoverItem && curItemIdx >= 0 && curItemIdx < visitems.length && isCurValid) return itemIndex;
534 moving = false;
535 needRedraw = true;
537 break;
538 default:
540 return NotMine;