hanging code cleanup; slightly easier hang
[k8vacspelynky.git] / uisimple.vc
blob112c0f1b6ff1a1d80417ee008313647a8ab89ba9
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 UIMenuItem : UIControl;
70 void delegate (UIMenuItem me) onSelected;
71 class!Object tagClass;
74 static final UIMenuItem Create (UIPane aowner, string acaption, string ahelp, optional void delegate (UIMenuItem me) seldg) {
75   UIMenuItem res = SpawnObject(UIMenuItem);
76   res.caption = acaption;
77   res.help = ahelp;
78   res.onSelected = seldg;
79   if (aowner) aowner.append(res);
80   return res;
84 override void drawWithOfs (int xofs, int yofs, int scale) {
85   setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
86   //owner.sprStore.renderText(xofs, yofs, caption, scale);
87   owner.sprStore.renderTextHiChar(xofs, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
91 // `true`: eaten
92 override bool onEvent (ref event_t evt) {
93   if (evt.type == ev_keydown) {
94     switch (evt.keycode) {
95       case K_ENTER: case K_SPACE:
96         if (onSelected) onSelected(self);
97         return true;
98     }
99   }
100   return false;
104 defaultproperties {
105   selectable = true;
109 // ////////////////////////////////////////////////////////////////////////// //
110 class UIKeyBinding : UIControl;
112 int *bind0, *bind1;
113 int currBindIdx;
114 int maxCaptionLength; // not scaled!
115 bool waitingKey;
116 void delegate (int bindidx, int newval) onValueChanged;
119 static final UIKeyBinding Create (UIPane aowner, int *abind0, int *abind1, string acaption, string ahelp) {
120   UIKeyBinding res = SpawnObject(UIKeyBinding);
121   res.caption = acaption;
122   res.help = ahelp;
123   res.bind0 = abind0;
124   res.bind1 = abind1;
125   if (aowner) {
126     aowner.append(res);
127     res.fixCaptionWidth();
128   }
129   return res;
133 override void blur () {
134   waitingKey = false;
135   ::blur();
139 private transient int tmpMaxCapWidth;
141 final void fixCaptionWidth () {
142   if (!owner) return;
143   tmpMaxCapWidth = 0;
144   owner.forEachControl(delegate bool (UIControl c) {
145     auto kb = UIKeyBinding(c);
146     if (kb) {
147       tmpMaxCapWidth = max(tmpMaxCapWidth, owner.sprStore.getTextWidth(kb.caption));
148     }
149     return false; // continue
150   });
151   owner.forEachControl(delegate bool (UIControl c) {
152     auto kb = UIKeyBinding(c);
153     if (kb) kb.maxCaptionLength = tmpMaxCapWidth;
154     return false; // continue
155   });
159 final void fixCurrBindIndex () {
160   if (!owner) return;
161   owner.forEachControl(delegate bool (UIControl c) {
162     if (c != self) {
163       auto kb = UIKeyBinding(c);
164       if (kb) kb.currBindIdx = currBindIdx;
165     }
166     return false; // continue
167   });
171 override void drawWithOfs (int xofs, int yofs, int scale) {
172   setTextColor(active ? 0xff_ff_00 : 0x9f_9f_9f);
173   //owner.sprStore.renderText(xofs, yofs, caption, scale);
174   owner.sprStore.renderTextHiChar(xofs, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
175   xofs += (maxCaptionLength+12)*scale;
176   setTextColor(0xff_ff_ff);
177   // first binding
178   string bname = (bind0 && *bind0 ? GetInputKeyStr(*bind0) : "---");
179   if (active) setTextColor(currBindIdx == 0 ? 0x00_ff_00 : 0xff_ff_ff); else setTextColor(0xff_ff_00);
180   if (waitingKey && currBindIdx == 0) { bname = "???"; setTextColor(0xff_00_00); }
181   owner.sprStore.renderText(xofs, yofs, bname, scale);
182   xofs += owner.sprStore.getTextWidth("WWWWWWWWWW")*scale;
183   // second binding
184   bname = (bind1 && *bind1 ? GetInputKeyStr(*bind1) : "---");
185   if (active) setTextColor(currBindIdx == 1 ? 0x00_ff_00 : 0xff_ff_ff); else setTextColor(0xff_ff_00);
186   if (waitingKey && currBindIdx == 1) { bname = "???"; setTextColor(0xff_00_00); }
187   owner.sprStore.renderText(xofs, yofs, bname, scale);
191 private transient int tmpKeyCode;
193 // `true`: eaten
194 override bool onEvent (ref event_t evt) {
195   if (waitingKey) {
196     if (evt.type == ev_keyup) return true;
197     if (evt.type == ev_keydown) {
198       if (evt.keycode >= K_F1 && evt.keycode <= K_F12) return true;
199       switch (evt.keycode) {
200         case K_ESCAPE: waitingKey = false; return true;
201       }
202       // remove keycode from other bindings
203       tmpKeyCode = evt.keycode;
204       owner.forEachControl(delegate bool (UIControl c) {
205         auto kb = UIKeyBinding(c);
206         if (kb) {
207           if (kb.bind0 && *kb.bind0 == tmpKeyCode) {
208             *kb.bind0 = 0;
209             if (kb.onValueChanged) kb.onValueChanged(0, 0);
210           }
211           if (kb.bind1 && *kb.bind1 == tmpKeyCode) {
212             *kb.bind1 = 0;
213             if (kb.onValueChanged) kb.onValueChanged(1, 0);
214           }
215         }
216         return false; // continue
217       });
218       if (currBindIdx == 0) *bind0 = evt.keycode; else *bind1 = evt.keycode;
219       waitingKey = false;
220       if (onValueChanged) onValueChanged(currBindIdx, evt.keycode);
221       return true;
222     }
223     return false;
224   }
225   if (evt.type == ev_keydown) {
226     switch (evt.keycode) {
227       case K_ENTER:
228         if (currBindIdx == 0 && !bind0) return true;
229         if (currBindIdx == 1 && !bind1) return true;
230         waitingKey = true;
231         return true;
232       case K_DELETE: case K_PADDOT:
233         // remove binding
234         if (currBindIdx == 0 && bind0 && *bind0) {
235           *bind0 = 0;
236           if (onValueChanged) onValueChanged(0, 0);
237         }
238         if (currBindIdx == 1 && bind1 && *bind1) {
239           *bind1 = 0;
240           if (onValueChanged) onValueChanged(1, 0);
241         }
242         break;
243       case K_LEFTARROW: case K_PAD4:
244         if (currBindIdx) {
245           currBindIdx = 0;
246           fixCurrBindIndex();
247         }
248         return true;
249       case K_RIGHTARROW: case K_PAD6:
250         if (!currBindIdx) {
251           currBindIdx = 1;
252           fixCurrBindIndex();
253         }
254         return true;
255     }
256   }
257   return false;
261 defaultproperties {
262   selectable = true;
266 // ////////////////////////////////////////////////////////////////////////// //
267 class UICheckBox : UIControl;
269 int *value;
270 void delegate (int newval) onValueChanged;
273 static final UICheckBox Create (UIPane aowner, int *avalue, string acaption, string ahelp) {
274   UICheckBox res = SpawnObject(UICheckBox);
275   res.caption = acaption;
276   res.help = ahelp;
277   res.value = avalue;
278   if (aowner) aowner.append(res);
279   return res;
283 override void drawWithOfs (int xofs, int yofs, int scale) {
284   setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
285   owner.sprStore.renderText(xofs, yofs, "[ ]", scale);
286   if (*value) {
287     setTextColor(0xff_00_00);
288     owner.sprStore.renderText(xofs+8*scale, yofs, "x", scale);
289     setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
290   }
291   owner.sprStore.renderTextHiChar(xofs+owner.sprStore.getTextWidth("[ ]", scale)+4*scale, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
295 // `true`: eaten
296 override bool onEvent (ref event_t evt) {
297   if (evt.type == ev_keydown) {
298     switch (evt.keycode) {
299       case K_ENTER: case K_SPACE:
300         *value = !(*value);
301         if (onValueChanged) onValueChanged(*value);
302         return true;
303     }
304   }
305   return false;
309 defaultproperties {
310   selectable = true;
314 // ////////////////////////////////////////////////////////////////////////// //
315 class UIIntEnum : UIControl;
318 int *value;
319 int vmin, vmax;
320 int step = 1;
321 array!string names;
322 string delegate (int val) getNameCB;
323 void delegate (int newval) onValueChanged;
326 static final UIIntEnum Create (UIPane aowner, int *avalue, int amin, int amax, string acaption, string ahelp) {
327   UIIntEnum res = SpawnObject(UIIntEnum);
328   res.caption = acaption;
329   res.help = ahelp;
330   res.value = avalue;
331   res.vmin = amin;
332   res.vmax = amax;
333   if (aowner) aowner.append(res);
334   return res;
338 override void Destroy () {
339   names.length = 0;
340   ::Destroy();
344 override void drawWithOfs (int xofs, int yofs, int scale) {
345   //owner.sprStore.renderText(xofs, yofs, "[ ]", scale);
346   int ofs = owner.sprStore.getTextWidth("[ ]", scale);
347   setTextColor(0x00_ff_00);
348   owner.sprStore.renderText(xofs+ofs/2-owner.sprStore.getTextWidth("<>", scale)/2, yofs, "<>", scale);
349   setTextColor(active ? 0xff_ff_00 : 0xff_ff_ff);
350   xofs += ofs+4*scale;
351   owner.sprStore.renderTextHiChar(xofs, yofs, caption, scale, hotCharOfs, buildColor(0xff_7f_00));
352   xofs += owner.sprStore.getTextWidth(caption, scale);
353   setTextColor(0xff_00_00);
354   string val;
355   if (getNameCB) val = getNameCB(*value);
356   if (!val && names.length) {
357     int n = *value-vmin;
358     if (n >= 0 && n < names.length) val = names[n];
359   }
360   if (!val) val = va("%d", *value);
361   owner.sprStore.renderText(xofs, yofs, val, scale);
365 // `true`: eaten
366 override bool onEvent (ref event_t evt) {
367   if (evt.type == ev_keydown) {
368     auto oval = *value;
369     switch (evt.keycode) {
370       case K_LEFTARROW: case K_PAD4:
371         *value = max(vmin, *value-step);
372         if (*value != oval && onValueChanged) onValueChanged(*value);
373         return true;
374       case K_RIGHTARROW: case K_PAD6:
375         *value = min(vmax, *value+step);
376         if (*value != oval && onValueChanged) onValueChanged(*value);
377         return true;
378     }
379   }
380   return false;
384 defaultproperties {
385   selectable = true;
389 // ////////////////////////////////////////////////////////////////////////// //
390 class UIPane : Object;
392 name id;
393 SpriteStore sprStore;
394 int width;
395 int height;
396 int scale = 3;
397 name fontName = 'sFontSmall';
399 bool closeMe;
401 array!UIControl controls;
402 int topItem;
403 int currItem;
406 struct SaveInfo {
407   int top, curr;
411 override void Destroy () {
412   foreach (ref auto ctl; controls) {
413     ctl.owner = none; // so it won't try to remove itself
414     delete ctl;
415   }
416   controls.length = 0;
417   ::Destroy();
421 final void saveState (out SaveInfo nfo) {
422   nfo.top = topItem;
423   nfo.curr = currItem;
427 final void restoreState (const ref SaveInfo nfo) {
428   setTopCurr(nfo.top, nfo.curr);
432 final void setupHotkeys () {
433   foreach (UIControl c; controls) {
434     if (!c.selectable) continue;
435     if (c.hotChar) continue;
436     c.hotCharOfs = -1;
437     // check for "~c"
438     foreach (auto sidx; 0..c.caption.length-1) {
439       auto ch = c.caption[sidx];
440       if (ch == "~") {
441         ch = c.caption[sidx+1];
442         if (ch >= "A" && ch <= "Z") ch += 32; // tolower
443         if ((ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "z")) {
444           c.hotCharOfs = sidx;
445           c.hotChar = ch;
446           c.caption[sidx..sidx+1] = ""; // remove "~"
447           break;
448         }
449       }
450     }
451     if (c.hotCharOfs >= 0) continue;
452     // autoassign
453     foreach (auto sidx; 0..c.caption.length) {
454       auto ch = c.caption[sidx];
455       if (ch >= "A" && ch <= "Z") ch += 32; // tolower
456       if ((ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "z")) {
457         // check if it doesn't conflict
458         bool conflictFound = false;
459         foreach (UIControl cc; controls) {
460           if (!cc.selectable) continue;
461           if (cc.hotChar == ch) {
462             // i found her!
463             conflictFound = true;
464             break;
465           }
466         }
467         if (!conflictFound) {
468           c.hotChar = ch;
469           c.hotCharOfs = sidx;
470           break;
471         }
472       }
473     }
474   }
478 final void setTopCurr (int newtop, int newcurr) {
479   if (newcurr < 0 || newcurr >= controls.length) return;
480   if (newcurr != currItem) {
481     if (currItem >= 0 && currItem <= controls.length) controls[currItem].blur();
482     currItem = newcurr;
483     controls[currItem].focus();
484   }
485   topItem = clamp(newtop, 0, controls.length-visLines+1);
486   makeCurrentItemVisible();
490 void append (UIControl ctl) {
491   if (!ctl) return;
492   if (ctl.owner == self) return;
493   if (ctl.owner) FatalError("UIControl already owned by another pane");
494   controls[$] = ctl;
495   ctl.owner = self;
496   if (ctl.selectable && currItem >= 0 && currItem < controls.length-1 && !controls[currItem].selectable) {
497     controls[currItem].blur();
498     currItem = controls.length-1;
499     controls[currItem].focus();
500   }
501   if (currItem >= 0 && currItem <= controls.length) controls[currItem].focus();
505 // return `true` from delegate to stop
506 final UIControl forEachControl (bool delegate (UIControl c) dg) {
507   if (!dg) return none;
508   foreach (UIControl c; controls) if (dg(c)) return c;
509   return none;
513 final int getLineHeight () {
514   sprStore.loadFont(fontName);
515   auto fh = sprStore.getFontHeight(scale);
516   if (fh < 1) fh = 8;
517   return fh;
521 final int visLines () {
522   return max(1, height/getLineHeight()-7);
526 final void makeCurrentItemVisible () {
527   auto clen = controls.length;
528   if (clen == 0 || currItem < 0 || currItem >= clen) return;
529   if (currItem < topItem) { topItem = currItem; return; }
530   auto vls = visLines();
531   if (currItem > topItem+vls-1) topItem = max(0, currItem-(vls-1));
535 void goItemUp () {
536   for (int n = currItem-1; n >= 0; --n) {
537     if (controls[n].selectable) {
538       if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
539       currItem = n;
540       controls[currItem].focus();
541       return;
542     }
543   }
547 void goItemDown () {
548   for (int n = currItem+1; n < controls.length; ++n) {
549     if (controls[n].selectable) {
550       if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
551       currItem = n;
552       controls[currItem].focus();
553       return;
554     }
555   }
559 void drawWithOfs (int xofs, int yofs) {
560   makeCurrentItemVisible();
561   sprStore.loadFont(fontName);
563   // "upscroll" mark
564   if (topItem > 0) {
565     Video.color = 0x3f_ff_ff_00;
566     auto spr = sprStore['sPageUp'];
567     spr.frames[0].tex.blitAt(xofs-9*scale, yofs, scale);
568   }
569   // "downscroll" mark
570   if (topItem+visLines < controls.length) {
571     Video.color = 0x3f_ff_ff_00;
572     auto spr = sprStore['sPageDown'];
573     spr.frames[0].tex.blitAt(xofs-9*scale, yofs+getLineHeight()*(visLines-1)-scale, scale);
574   }
576   int curY = yofs;
577   foreach (int n; topItem..topItem+visLines) {
578     if (n >= 0) {
579       if (n >= controls.length) break;
580       controls[n].drawWithOfs(xofs, yofs, scale);
581     }
582     yofs += getLineHeight();
583   }
585   // help
586   if (currItem >= 0 && currItem < controls.length) {
587     string help = controls[currItem].help;
588     yofs += getLineHeight();
589     if (help) {
590       Video.color = 0xff_ff_00;
591       sprStore.renderTextWrapped(xofs, yofs, 36*8*scale, help, scale);
592     }
593   }
597 // `true`: eaten
598 bool onEvent (ref event_t evt) {
599   if (currItem >= 0 && currItem < controls.length) {
600     if (controls[currItem].onEvent(evt)) return true;
601   }
602   if (evt.type == ev_keydown) {
603     switch (evt.keycode) {
604       case K_UPARROW: case K_PAD8: goItemUp(); return true;
605       case K_DOWNARROW: case K_PAD2: goItemDown(); return true;
606       case K_HOME: case K_PAD7:
607         if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
608         currItem = 0;
609         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
610         return true;
611       case K_END: case K_PAD1:
612         if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
613         if (controls.length) currItem = controls.length-1;
614         if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
615         return true;
616       case K_PAGEUP: case K_PAD9:
617         if (controls.length < visLines) return true;
618         makeCurrentItemVisible();
619         controls[currItem].blur();
620         if (currItem != topItem) {
621           currItem = topItem;
622         } else {
623           currItem = max(0, currItem-(visLines-1));
624         }
625         controls[currItem].focus();
626         break;
627       case K_PAGEDOWN: case K_PAD3:
628         if (controls.length < visLines) return true;
629         makeCurrentItemVisible();
630         controls[currItem].blur();
631         if (currItem != topItem+visLines-1) {
632           currItem = min(controls.length-1, topItem+visLines-1);
633         } else {
634           currItem = min(controls.length-1, currItem+(visLines-1));
635         }
636         controls[currItem].focus();
637         break;
638       default:
639         // hotkey
640         if ((evt.keycode >= K_N0 && evt.keycode <= K_N9) || (evt.keycode >= K_a && evt.keycode <= K_z)) {
641           int newidx = -1;
642           foreach (auto cidx, UIControl c; controls) {
643             if (!c.selectable) continue;
644             if (c.hotChar == evt.keycode) {
645               // i found her!
646               newidx = cidx;
647               break;
648             }
649           }
650           if (newidx >= 0 && newidx < controls.length && newidx != currItem) {
651             if (currItem >= 0 && currItem < controls.length) controls[currItem].blur();
652             currItem = newidx;
653             if (currItem >= 0 && currItem < controls.length) controls[currItem].focus();
654           }
655           return (newidx >= 0);
656         }
657         break;
658     }
659   }
660   return false;