F1 now shows game map
[k8vacspelynky.git] / uisimple.vc
blobaee6ee9a39907b16db9ac30044b47d79ef4e14a1
1 /**************************************************************************
2  *    This program is free software; you can redistribute it and/or
3  *  modify it under the terms of the GNU General Public License
4  *  as published by the Free Software Foundation; either version 3
5  *  of the License, or (at your option) any later version.
6  *
7  *    This program is distributed in the hope that it will be useful,
8  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
9  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  *  GNU General Public License for more details.
11  **************************************************************************/
12 class UIControl : Object;
14 UIPane owner;
15 name id;
16 string caption;
17 string help;
18 bool selectable;
19 bool active;
20 int hotChar;
21 int hotCharOfs = -1; // in chars
24 override void Destroy () {
25   if (owner) owner.remove(self);
26   ::Destroy();
31 final int buildColor (int clr) {
32   return (clr&0xff_ff_ff)|(Video.color&0xff_00_00_00);
36 void setTextColor (int clr) {
37   int alpha = 0;
38   if (active) {
39     alpha = int(GetTickCount()*200)%255;
40     alpha = clamp(alpha > 127 ? 255-(alpha-128)*2 : alpha*2, 0, 255);
41     alpha = clamp(alpha-alpha/4, 0, 255);
42   }
43   Video.color = (clr&0xff_ff_ff)|(alpha<<24);
47 void drawWithOfs (int xofs, int yofs, int scale) {
51 void focus () {
52   active = true;
56 void blur () {
57   active = false;
61 // `true`: eaten
62 bool onEvent (ref event_t evt) {
63   return false;
67 // ////////////////////////////////////////////////////////////////////////// //
68 class UILabel : UIControl;
71 static final UILabel Create (UIPane aowner, string acaption) {
72   UILabel res = SpawnObject(UILabel);
73   res.caption = acaption;
74   if (aowner) aowner.append(res);
75   return res;
79 override void drawWithOfs (int xofs, int yofs, int scale) {
80   setTextColor(0x00_ff_ff);
81   owner.sprStore.renderText(xofs+12*scale, yofs, caption, scale);
85 defaultproperties {
86   selectable = false;
90 // ////////////////////////////////////////////////////////////////////////// //
91 class UIMenuItem : UIControl;
93 void delegate (UIMenuItem me) onSelected;
94 class!Object tagClass;
97 static final UIMenuItem Create (UIPane aowner, string acaption, string ahelp, optional void delegate (UIMenuItem me) seldg) {
98   UIMenuItem res = SpawnObject(UIMenuItem);
99   res.caption = acaption;
100   res.help = ahelp;
101   res.onSelected = seldg;
102   if (aowner) aowner.append(res);
103   return res;
107 override void drawWithOfs (int xofs, int yofs, int scale) {
108   setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
109   //owner.sprStore.renderText(xofs, yofs, caption, scale);
110   owner.sprStore.renderTextHiChar(xofs, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
114 // `true`: eaten
115 override bool onEvent (ref event_t evt) {
116   if (evt.type == ev_keydown) {
117     switch (evt.keycode) {
118       case K_ENTER: case K_SPACE:
119         if (onSelected) onSelected(self);
120         return true;
121     }
122   }
123   return false;
127 defaultproperties {
128   selectable = true;
132 // ////////////////////////////////////////////////////////////////////////// //
133 class UIKeyBinding : UIControl;
135 int *bind0, *bind1;
136 int currBindIdx;
137 int maxCaptionLength; // not scaled!
138 bool waitingKey;
139 void delegate (int bindidx, int newval) onValueChanged;
142 static final UIKeyBinding Create (UIPane aowner, int *abind0, int *abind1, string acaption, string ahelp) {
143   UIKeyBinding res = SpawnObject(UIKeyBinding);
144   res.caption = acaption;
145   res.help = ahelp;
146   res.bind0 = abind0;
147   res.bind1 = abind1;
148   if (aowner) {
149     aowner.append(res);
150     res.fixCaptionWidth();
151   }
152   return res;
156 override void blur () {
157   waitingKey = false;
158   ::blur();
162 private transient int tmpMaxCapWidth;
164 final void fixCaptionWidth () {
165   if (!owner) return;
166   tmpMaxCapWidth = 0;
167   owner.forEachControl(delegate bool (UIControl c) {
168     auto kb = UIKeyBinding(c);
169     if (kb) {
170       tmpMaxCapWidth = max(tmpMaxCapWidth, owner.sprStore.getTextWidth(kb.caption));
171     }
172     return false; // continue
173   });
174   owner.forEachControl(delegate bool (UIControl c) {
175     auto kb = UIKeyBinding(c);
176     if (kb) kb.maxCaptionLength = tmpMaxCapWidth;
177     return false; // continue
178   });
182 final void fixCurrBindIndex () {
183   if (!owner) return;
184   owner.forEachControl(delegate bool (UIControl c) {
185     if (c != self) {
186       auto kb = UIKeyBinding(c);
187       if (kb) kb.currBindIdx = currBindIdx;
188     }
189     return false; // continue
190   });
194 override void drawWithOfs (int xofs, int yofs, int scale) {
195   setTextColor(active ? 0xff_ff_00 : 0x9f_9f_9f);
196   //owner.sprStore.renderText(xofs, yofs, caption, scale);
197   owner.sprStore.renderTextHiChar(xofs, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
198   xofs += (maxCaptionLength+12)*scale;
199   setTextColor(0xff_ff_ff);
200   // first binding
201   string bname = (bind0 && *bind0 ? GetInputKeyStrName(*bind0) : "---");
202   if (active) setTextColor(currBindIdx == 0 ? 0x00_ff_00 : 0xff_ff_ff); else setTextColor(0xff_ff_00);
203   if (waitingKey && currBindIdx == 0) { bname = "???"; setTextColor(0xff_00_00); }
204   owner.sprStore.renderText(xofs, yofs, bname, scale);
205   xofs += owner.sprStore.getTextWidth("WWWWWWWWWW")*scale;
206   // second binding
207   bname = (bind1 && *bind1 ? GetInputKeyStrName(*bind1) : "---");
208   if (active) setTextColor(currBindIdx == 1 ? 0x00_ff_00 : 0xff_ff_ff); else setTextColor(0xff_ff_00);
209   if (waitingKey && currBindIdx == 1) { bname = "???"; setTextColor(0xff_00_00); }
210   owner.sprStore.renderText(xofs, yofs, bname, scale);
214 private transient int tmpKeyCode;
216 // `true`: eaten
217 override bool onEvent (ref event_t evt) {
218   if (waitingKey) {
219     if (evt.type == ev_keyup) return true;
220     if (evt.type == ev_keydown) {
221       if (evt.keycode >= K_F1 && evt.keycode <= K_F12) return true;
222       switch (evt.keycode) {
223         case K_ESCAPE: waitingKey = false; return true;
224       }
225       // remove keycode from other bindings
226       tmpKeyCode = evt.keycode;
227       owner.forEachControl(delegate bool (UIControl c) {
228         auto kb = UIKeyBinding(c);
229         if (kb) {
230           if (kb.bind0 && *kb.bind0 == tmpKeyCode) {
231             *kb.bind0 = 0;
232             if (kb.onValueChanged) kb.onValueChanged(0, 0);
233           }
234           if (kb.bind1 && *kb.bind1 == tmpKeyCode) {
235             *kb.bind1 = 0;
236             if (kb.onValueChanged) kb.onValueChanged(1, 0);
237           }
238         }
239         return false; // continue
240       });
241       if (currBindIdx == 0) *bind0 = evt.keycode; else *bind1 = evt.keycode;
242       waitingKey = false;
243       if (onValueChanged) onValueChanged(currBindIdx, evt.keycode);
244       return true;
245     }
246     return false;
247   }
248   if (evt.type == ev_keydown) {
249     switch (evt.keycode) {
250       case K_ENTER:
251         if (currBindIdx == 0 && !bind0) return true;
252         if (currBindIdx == 1 && !bind1) return true;
253         waitingKey = true;
254         return true;
255       case K_DELETE: case K_PADDOT:
256         // remove binding
257         if (currBindIdx == 0 && bind0 && *bind0) {
258           *bind0 = 0;
259           if (onValueChanged) onValueChanged(0, 0);
260         }
261         if (currBindIdx == 1 && bind1 && *bind1) {
262           *bind1 = 0;
263           if (onValueChanged) onValueChanged(1, 0);
264         }
265         break;
266       case K_LEFTARROW: case K_PAD4:
267         if (currBindIdx) {
268           currBindIdx = 0;
269           fixCurrBindIndex();
270         }
271         return true;
272       case K_RIGHTARROW: case K_PAD6:
273         if (!currBindIdx) {
274           currBindIdx = 1;
275           fixCurrBindIndex();
276         }
277         return true;
278     }
279   }
280   return false;
284 defaultproperties {
285   selectable = true;
289 // ////////////////////////////////////////////////////////////////////////// //
290 class UICheckBox : UIControl;
292 int *value;
293 void delegate (int newval) onValueChanged;
296 static final UICheckBox Create (UIPane aowner, int *avalue, string acaption, string ahelp) {
297   UICheckBox res = SpawnObject(UICheckBox);
298   res.caption = acaption;
299   res.help = ahelp;
300   res.value = avalue;
301   if (aowner) aowner.append(res);
302   return res;
306 override void drawWithOfs (int xofs, int yofs, int scale) {
307   setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
308   owner.sprStore.renderText(xofs, yofs, "[ ]", scale);
309   if (*value) {
310     setTextColor(0xff_00_00);
311     owner.sprStore.renderText(xofs+8*scale, yofs, "x", scale);
312     setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
313   }
314   owner.sprStore.renderTextHiChar(xofs+owner.sprStore.getTextWidth("[ ]", scale)+4*scale, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
318 // `true`: eaten
319 override bool onEvent (ref event_t evt) {
320   if (evt.type == ev_keydown) {
321     switch (evt.keycode) {
322       case K_ENTER: case K_SPACE:
323         *value = !(*value);
324         if (onValueChanged) onValueChanged(*value);
325         return true;
326     }
327   }
328   return false;
332 defaultproperties {
333   selectable = true;
337 // ////////////////////////////////////////////////////////////////////////// //
338 class UIIntEnum : UIControl;
341 int *value;
342 int vmin, vmax;
343 int step = 1;
344 array!string names;
345 string delegate (int val) getNameCB;
346 void delegate (int newval) onValueChanged;
349 static final UIIntEnum Create (UIPane aowner, int *avalue, int amin, int amax, string acaption, string ahelp) {
350   UIIntEnum res = SpawnObject(UIIntEnum);
351   res.caption = acaption;
352   res.help = ahelp;
353   res.value = avalue;
354   res.vmin = amin;
355   res.vmax = amax;
356   if (aowner) aowner.append(res);
357   return res;
361 override void Destroy () {
362   names.length = 0;
363   ::Destroy();
367 override void drawWithOfs (int xofs, int yofs, int scale) {
368   //owner.sprStore.renderText(xofs, yofs, "[ ]", scale);
369   int ofs = owner.sprStore.getTextWidth("[ ]", scale);
370   setTextColor(0x00_ff_00);
371   owner.sprStore.renderText(xofs+ofs/2-owner.sprStore.getTextWidth("<>", scale)/2, yofs, "<>", scale);
372   setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
373   xofs += ofs+4*scale;
374   owner.sprStore.renderTextHiChar(xofs, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
375   xofs += owner.sprStore.getTextWidth(caption, scale);
376   setTextColor(0xff_00_00);
377   string val;
378   if (getNameCB) val = getNameCB(*value);
379   if (!val && names.length) {
380     int n = *value-vmin;
381     if (n >= 0 && n < names.length) val = names[n];
382   }
383   if (!val) val = va("%d", *value);
384   owner.sprStore.renderText(xofs, yofs, val, scale);
388 // `true`: eaten
389 override bool onEvent (ref event_t evt) {
390   if (evt.type == ev_keydown) {
391     auto oval = *value;
392     switch (evt.keycode) {
393       case K_LEFTARROW: case K_PAD4:
394         *value = max(vmin, *value-step);
395         if (*value != oval && onValueChanged) onValueChanged(*value);
396         return true;
397       case K_RIGHTARROW: case K_PAD6:
398         *value = min(vmax, *value+step);
399         if (*value != oval && onValueChanged) onValueChanged(*value);
400         return true;
401     }
402   }
403   return false;
407 defaultproperties {
408   selectable = true;
412 // ////////////////////////////////////////////////////////////////////////// //
413 class UIPane : Object;
415 name id;
416 SpriteStore sprStore;
417 int width;
418 int height;
419 int scale = 3;
420 name fontName = 'sFontSmall';
422 bool closeMe;
424 array!UIControl controls;
425 int topItem;
426 int currItem;
429 struct SaveInfo {
430   int top, curr;
434 override void Destroy () {
435   foreach (ref auto ctl; controls) {
436     ctl.owner = none; // so it won't try to remove itself
437     delete ctl;
438   }
439   controls.length = 0;
440   ::Destroy();
444 final void saveState (out SaveInfo nfo) {
445   nfo.top = topItem;
446   nfo.curr = currItem;
450 final void restoreState (const ref SaveInfo nfo) {
451   setTopCurr(nfo.top, nfo.curr);
455 final void setupHotkeys () {
456   foreach (UIControl c; controls) {
457     if (!c.selectable) continue;
458     if (c.hotChar) continue;
459     c.hotCharOfs = -1;
460     // check for "~c"
461     foreach (auto sidx; 0..c.caption.length-1) {
462       auto ch = c.caption[sidx];
463       if (ch == "~") {
464         ch = c.caption[sidx+1];
465         if (ch >= "A" && ch <= "Z") ch += 32; // tolower
466         if ((ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "z")) {
467           c.hotCharOfs = sidx;
468           c.hotChar = ch;
469           c.caption[sidx..sidx+1] = ""; // remove "~"
470           break;
471         }
472       }
473     }
474     if (c.hotCharOfs >= 0) continue;
475     // autoassign
476     foreach (auto sidx; 0..c.caption.length) {
477       auto ch = c.caption[sidx];
478       if (ch >= "A" && ch <= "Z") ch += 32; // tolower
479       if ((ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "z")) {
480         // check if it doesn't conflict
481         bool conflictFound = false;
482         foreach (UIControl cc; controls) {
483           if (!cc.selectable) continue;
484           if (cc.hotChar == ch) {
485             // i found her!
486             conflictFound = true;
487             break;
488           }
489         }
490         if (!conflictFound) {
491           c.hotChar = ch;
492           c.hotCharOfs = sidx;
493           break;
494         }
495       }
496     }
497   }
501 final void setTopCurr (int newtop, int newcurr) {
502   if (newcurr < 0 || newcurr >= controls.length) return;
503   if (newcurr != currItem) {
504     if (currItem >= 0 && currItem <= controls.length) controls[currItem].blur();
505     currItem = newcurr;
506     controls[currItem].focus();
507   }
508   topItem = clamp(newtop, 0, controls.length-visLines+1);
509   makeCurrentItemVisible();
513 void append (UIControl ctl) {
514   if (!ctl) return;
515   if (ctl.owner == self) return;
516   if (ctl.owner) FatalError("UIControl already owned by another pane");
517   controls[$] = ctl;
518   ctl.owner = self;
519   if (ctl.selectable && currItem >= 0 && currItem < controls.length-1 && !controls[currItem].selectable) {
520     controls[currItem].blur();
521     currItem = controls.length-1;
522     controls[currItem].focus();
523   }
524   if (currItem >= 0 && currItem <= controls.length) controls[currItem].focus();
528 // return `true` from delegate to stop
529 final UIControl forEachControl (bool delegate (UIControl c) dg) {
530   if (!dg) return none;
531   foreach (UIControl c; controls) if (dg(c)) return c;
532   return none;
536 final int getLineHeight () {
537   sprStore.loadFont(fontName);
538   auto fh = sprStore.getFontHeight(scale);
539   if (fh < 1) fh = 8;
540   return fh;
544 final int visLines () {
545   return max(1, height/getLineHeight()-7);
549 final void makeCurrentItemVisible () {
550   auto clen = controls.length;
551   if (clen == 0 || currItem < 0 || currItem >= clen) return;
552   if (currItem < topItem) { topItem = currItem; return; }
553   auto vls = visLines();
554   if (currItem > topItem+vls-1) topItem = max(0, currItem-(vls-1));
558 void goItemUp () {
559   for (int n = currItem-1; n >= 0; --n) {
560     if (controls[n].selectable) {
561       if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
562       currItem = n;
563       controls[currItem].focus();
564       return;
565     }
566   }
567   topItem = 0;
568   int n = 0;
569   while (n < controls.length && !controls[n].selectable) ++n;
570   if (n >= controls.length) return;
571   if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
572   currItem = n;
573   if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
577 void goItemDown () {
578   for (int n = currItem+1; n < controls.length; ++n) {
579     if (controls[n].selectable) {
580       if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
581       currItem = n;
582       controls[currItem].focus();
583       return;
584     }
585   }
589 void drawWithOfs (int xofs, int yofs) {
590   makeCurrentItemVisible();
591   sprStore.loadFont(fontName);
593   // "upscroll" mark
594   if (topItem > 0) {
595     Video.color = 0x3f_ff_ff_00;
596     auto spr = sprStore['sPageUp'];
597     spr.frames[0].tex.blitAt(xofs-9*scale, yofs, scale);
598   }
599   // "downscroll" mark
600   if (topItem+visLines < controls.length) {
601     Video.color = 0x3f_ff_ff_00;
602     auto spr = sprStore['sPageDown'];
603     spr.frames[0].tex.blitAt(xofs-9*scale, yofs+getLineHeight()*(visLines-1)-scale, scale);
604   }
606   int curY = yofs;
607   foreach (int n; topItem..topItem+visLines) {
608     if (n >= 0) {
609       if (n >= controls.length) break;
610       controls[n].drawWithOfs(xofs, yofs, scale);
611     }
612     yofs += getLineHeight();
613   }
615   // help
616   if (currItem >= 0 && currItem < controls.length) {
617     string help = controls[currItem].help;
618     yofs += getLineHeight();
619     if (help) {
620       Video.color = 0xff_ff_00;
621       sprStore.renderTextWrapped(xofs, yofs, 36*8*scale, help, scale);
622     }
623   }
627 // `true`: eaten
628 bool onEvent (ref event_t evt) {
629   if (currItem >= 0 && currItem < controls.length) {
630     if (controls[currItem].onEvent(evt)) return true;
631   }
632   if (evt.type == ev_keydown) {
633     switch (evt.keycode) {
634       case K_UPARROW: case K_PAD8: goItemUp(); return true;
635       case K_DOWNARROW: case K_PAD2: goItemDown(); return true;
636       case K_HOME: case K_PAD7:
637         if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
638         currItem = 0;
639         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
640         return true;
641       case K_END: case K_PAD1:
642         if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
643         if (controls.length) currItem = controls.length-1;
644         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
645         return true;
646       case K_PAGEUP: case K_PAD9:
647         if (controls.length < visLines) return true;
648         auto oldItemIndex = currItem;
649         makeCurrentItemVisible();
650         controls[currItem].blur();
651         if (currItem != topItem) {
652           currItem = topItem;
653         } else {
654           currItem = max(0, currItem-(visLines-1));
655         }
656         while (currItem >= 0 && currItem != oldItemIndex && !controls[currItem].selectable) --currItem;
657         if (currItem < 0) { topItem = 0; currItem = oldItemIndex; }
658         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
659         break;
660       case K_PAGEDOWN: case K_PAD3:
661         if (controls.length < visLines) return true;
662         oldItemIndex = currItem;
663         makeCurrentItemVisible();
664         controls[currItem].blur();
665         if (currItem != topItem+visLines-1) {
666           currItem = min(controls.length-1, topItem+visLines-1);
667         } else {
668           currItem = min(controls.length-1, currItem+(visLines-1));
669         }
670         while (currItem < controls.length && currItem != oldItemIndex && !controls[currItem].selectable) ++currItem;
671         if (currItem >= controls.length) currItem = oldItemIndex;
672         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
673         break;
674       default:
675         // hotkey
676         if ((evt.keycode >= K_N0 && evt.keycode <= K_N9) || (evt.keycode >= K_a && evt.keycode <= K_z)) {
677           int newidx = -1;
678           foreach (auto cidx, UIControl c; controls) {
679             if (!c.selectable) continue;
680             if (c.hotChar == evt.keycode) {
681               // i found her!
682               newidx = cidx;
683               break;
684             }
685           }
686           if (newidx >= 0 && newidx < controls.length && newidx != currItem) {
687             if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
688             currItem = newidx;
689             if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
690           }
691           return (newidx >= 0);
692         }
693         break;
694     }
695   }
696   return false;