player can meet Tunnel Man now (and open shortcuts)
[k8vacspelynky.git] / uisimple.vc
blob9a8bc4f346fad8e05dae2bc11bbf3bac99055146
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   auto sbox = owner.sprStore[*value ? 'sBoxChecked' : 'sBox'];
309   auto frm = sbox.frames[0];
310   frm.tex.blitAt(xofs, yofs, scale);
311   owner.sprStore.renderTextHiChar(xofs+(frm.width+2)*scale, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
312   /*;
313   owner.sprStore.renderText(xofs, yofs, "[ ]", scale);
314   if (*value) {
315     setTextColor(0xff_00_00);
316     owner.sprStore.renderText(xofs+8*scale, yofs, "x", scale);
317     setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
318   }
319   owner.sprStore.renderTextHiChar(xofs+owner.sprStore.getTextWidth("[ ]", scale)+4*scale, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
320   */
324 // `true`: eaten
325 override bool onEvent (ref event_t evt) {
326   if (evt.type == ev_keydown) {
327     switch (evt.keycode) {
328       case K_ENTER: case K_SPACE:
329         *value = !(*value);
330         if (onValueChanged) onValueChanged(*value);
331         return true;
332     }
333   }
334   return false;
338 defaultproperties {
339   selectable = true;
343 // ////////////////////////////////////////////////////////////////////////// //
344 class UIIntEnum : UIControl;
347 int *value;
348 int vmin, vmax;
349 int step = 1;
350 array!string names;
351 string delegate (int val) getNameCB;
352 void delegate (int newval) onValueChanged;
355 static final UIIntEnum Create (UIPane aowner, int *avalue, int amin, int amax, string acaption, string ahelp) {
356   UIIntEnum res = SpawnObject(UIIntEnum);
357   res.caption = acaption;
358   res.help = ahelp;
359   res.value = avalue;
360   res.vmin = amin;
361   res.vmax = amax;
362   if (aowner) aowner.append(res);
363   return res;
367 override void Destroy () {
368   names.length = 0;
369   ::Destroy();
373 override void drawWithOfs (int xofs, int yofs, int scale) {
374   //owner.sprStore.renderText(xofs, yofs, "[ ]", scale);
375   auto sbox = owner.sprStore['sBox'];
376   auto frm = sbox.frames[0];
377   int ofs = (frm.width+2)*scale;
378   setTextColor(0x00_ff_00);
379   owner.sprStore.renderText(xofs, yofs, "<", scale);
380   owner.sprStore.renderText(xofs+ofs-owner.sprStore.getTextWidth(">", scale), yofs, ">", scale);
381   setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
382   xofs += ofs;
383   owner.sprStore.renderTextHiChar(xofs, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
384   xofs += owner.sprStore.getTextWidth(caption, scale);
385   setTextColor(0xff_00_00);
386   string val;
387   if (getNameCB) val = getNameCB(*value);
388   if (!val && names.length) {
389     int n = *value-vmin;
390     if (n >= 0 && n < names.length) val = names[n];
391   }
392   if (!val) val = va("%d", *value);
393   owner.sprStore.renderText(xofs, yofs, val, scale);
397 // `true`: eaten
398 override bool onEvent (ref event_t evt) {
399   if (evt.type == ev_keydown) {
400     auto oval = *value;
401     switch (evt.keycode) {
402       case K_LEFTARROW: case K_PAD4:
403         *value = max(vmin, *value-step);
404         if (*value != oval && onValueChanged) onValueChanged(*value);
405         return true;
406       case K_RIGHTARROW: case K_PAD6:
407         *value = min(vmax, *value+step);
408         if (*value != oval && onValueChanged) onValueChanged(*value);
409         return true;
410     }
411   }
412   return false;
416 defaultproperties {
417   selectable = true;
421 // ////////////////////////////////////////////////////////////////////////// //
422 class UIPane : Object;
424 name id;
425 SpriteStore sprStore;
426 int width;
427 int height;
428 int scale = 3;
429 name fontName = 'sFontSmall';
431 bool closeMe;
433 array!UIControl controls;
434 int topItem;
435 int currItem;
438 struct SaveInfo {
439   int top, curr;
443 override void Destroy () {
444   foreach (ref auto ctl; controls) {
445     ctl.owner = none; // so it won't try to remove itself
446     delete ctl;
447   }
448   controls.length = 0;
449   ::Destroy();
453 final void saveState (out SaveInfo nfo) {
454   nfo.top = topItem;
455   nfo.curr = currItem;
459 final void restoreState (const ref SaveInfo nfo) {
460   setTopCurr(nfo.top, nfo.curr);
464 final void setupHotkeys () {
465   foreach (UIControl c; controls) {
466     if (!c.selectable) continue;
467     if (c.hotChar) continue;
468     c.hotCharOfs = -1;
469     // check for "~c"
470     foreach (auto sidx; 0..c.caption.length-1) {
471       auto ch = c.caption[sidx];
472       if (ch == "~") {
473         ch = c.caption[sidx+1];
474         if (ch >= "A" && ch <= "Z") ch += 32; // tolower
475         if ((ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "z")) {
476           c.hotCharOfs = sidx;
477           c.hotChar = ch;
478           c.caption[sidx..sidx+1] = ""; // remove "~"
479           break;
480         }
481       }
482     }
483     if (c.hotCharOfs >= 0) continue;
484     // autoassign
485     foreach (auto sidx; 0..c.caption.length) {
486       auto ch = c.caption[sidx];
487       if (ch >= "A" && ch <= "Z") ch += 32; // tolower
488       if ((ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "z")) {
489         // check if it doesn't conflict
490         bool conflictFound = false;
491         foreach (UIControl cc; controls) {
492           if (!cc.selectable) continue;
493           if (cc.hotChar == ch) {
494             // i found her!
495             conflictFound = true;
496             break;
497           }
498         }
499         if (!conflictFound) {
500           c.hotChar = ch;
501           c.hotCharOfs = sidx;
502           break;
503         }
504       }
505     }
506   }
510 final void setTopCurr (int newtop, int newcurr) {
511   if (newcurr < 0 || newcurr >= controls.length) return;
512   if (newcurr != currItem) {
513     if (currItem >= 0 && currItem <= controls.length) controls[currItem].blur();
514     currItem = newcurr;
515     controls[currItem].focus();
516   }
517   topItem = clamp(newtop, 0, controls.length-visLines+1);
518   makeCurrentItemVisible();
522 void append (UIControl ctl) {
523   if (!ctl) return;
524   if (ctl.owner == self) return;
525   if (ctl.owner) FatalError("UIControl already owned by another pane");
526   controls[$] = ctl;
527   ctl.owner = self;
528   if (ctl.selectable && currItem >= 0 && currItem < controls.length-1 && !controls[currItem].selectable) {
529     controls[currItem].blur();
530     currItem = controls.length-1;
531     controls[currItem].focus();
532   }
533   if (currItem >= 0 && currItem <= controls.length) controls[currItem].focus();
537 // return `true` from delegate to stop
538 final UIControl forEachControl (bool delegate (UIControl c) dg) {
539   if (!dg) return none;
540   foreach (UIControl c; controls) if (dg(c)) return c;
541   return none;
545 final int getLineHeight () {
546   sprStore.loadFont(fontName);
547   auto fh = sprStore.getFontHeight(scale);
548   if (fh < 1) fh = 8;
549   return fh;
553 final int visLines () {
554   return max(1, height/getLineHeight()-7);
558 final void makeCurrentItemVisible () {
559   auto clen = controls.length;
560   if (clen == 0 || currItem < 0 || currItem >= clen) return;
561   if (currItem < topItem) { topItem = currItem; return; }
562   auto vls = visLines();
563   if (currItem > topItem+vls-1) topItem = max(0, currItem-(vls-1));
567 void goItemUp () {
568   for (int n = currItem-1; n >= 0; --n) {
569     if (controls[n].selectable) {
570       if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
571       currItem = n;
572       controls[currItem].focus();
573       return;
574     }
575   }
576   topItem = 0;
577   int n = 0;
578   while (n < controls.length && !controls[n].selectable) ++n;
579   if (n >= controls.length) return;
580   if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
581   currItem = n;
582   if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
586 void goItemDown () {
587   for (int n = currItem+1; n < controls.length; ++n) {
588     if (controls[n].selectable) {
589       if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
590       currItem = n;
591       controls[currItem].focus();
592       return;
593     }
594   }
598 void drawWithOfs (int xofs, int yofs) {
599   makeCurrentItemVisible();
600   sprStore.loadFont(fontName);
602   // "upscroll" mark
603   if (topItem > 0) {
604     Video.color = 0x3f_ff_ff_00;
605     auto spr = sprStore['sPageUp'];
606     spr.frames[0].tex.blitAt(xofs-9*scale, yofs, scale);
607   }
608   // "downscroll" mark
609   if (topItem+visLines < controls.length) {
610     Video.color = 0x3f_ff_ff_00;
611     auto spr = sprStore['sPageDown'];
612     spr.frames[0].tex.blitAt(xofs-9*scale, yofs+getLineHeight()*(visLines-1)-scale, scale);
613   }
615   int curY = yofs;
616   foreach (int n; topItem..topItem+visLines) {
617     if (n >= 0) {
618       if (n >= controls.length) break;
619       controls[n].drawWithOfs(xofs, yofs, scale);
620     }
621     yofs += getLineHeight();
622   }
624   // help
625   if (currItem >= 0 && currItem < controls.length) {
626     string help = controls[currItem].help;
627     yofs += getLineHeight();
628     if (help) {
629       Video.color = 0xff_ff_00;
630       sprStore.renderTextWrapped(xofs, yofs, 36*8*scale, help, scale);
631     }
632   }
636 // `true`: eaten
637 bool onEvent (ref event_t evt) {
638   if (currItem >= 0 && currItem < controls.length) {
639     if (controls[currItem].onEvent(evt)) return true;
640   }
641   if (evt.type == ev_keydown) {
642     switch (evt.keycode) {
643       case K_UPARROW: case K_PAD8: goItemUp(); return true;
644       case K_DOWNARROW: case K_PAD2: goItemDown(); return true;
645       case K_HOME: case K_PAD7:
646         if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
647         currItem = 0;
648         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
649         return true;
650       case K_END: case K_PAD1:
651         if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
652         if (controls.length) currItem = controls.length-1;
653         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
654         return true;
655       case K_PAGEUP: case K_PAD9:
656         if (controls.length < visLines) return true;
657         auto oldItemIndex = currItem;
658         makeCurrentItemVisible();
659         controls[currItem].blur();
660         if (currItem != topItem) {
661           currItem = topItem;
662         } else {
663           currItem = max(0, currItem-(visLines-1));
664         }
665         while (currItem >= 0 && currItem != oldItemIndex && !controls[currItem].selectable) --currItem;
666         if (currItem < 0) { topItem = 0; currItem = oldItemIndex; }
667         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
668         break;
669       case K_PAGEDOWN: case K_PAD3:
670         if (controls.length < visLines) return true;
671         oldItemIndex = currItem;
672         makeCurrentItemVisible();
673         controls[currItem].blur();
674         if (currItem != topItem+visLines-1) {
675           currItem = min(controls.length-1, topItem+visLines-1);
676         } else {
677           currItem = min(controls.length-1, currItem+(visLines-1));
678         }
679         while (currItem < controls.length && currItem != oldItemIndex && !controls[currItem].selectable) ++currItem;
680         if (currItem >= controls.length) currItem = oldItemIndex;
681         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
682         break;
683       default:
684         // hotkey
685         if ((evt.keycode >= K_N0 && evt.keycode <= K_N9) || (evt.keycode >= K_a && evt.keycode <= K_z)) {
686           int newidx = -1;
687           foreach (auto cidx, UIControl c; controls) {
688             if (!c.selectable) continue;
689             if (c.hotChar == evt.keycode) {
690               // i found her!
691               newidx = cidx;
692               break;
693             }
694           }
695           if (newidx >= 0 && newidx < controls.length && newidx != currItem) {
696             if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
697             currItem = newidx;
698             if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
699           }
700           return (newidx >= 0);
701         }
702         break;
703     }
704   }
705   return false;