erga: use `widgetChanged()` instead of direct screen rebuild posting; fixes to editor...
[iv.d.git] / egra / gui / widgets.d
blob50fbe00c8742362c91f34f14a2803a13c003e869
1 /*
2 * Simple Framebuffer Gfx/GUI lib
4 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
5 * Understanding is not required. Only obedience.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, version 3 of the License ONLY.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 module iv.egra.gui.widgets /*is aliced*/;
20 private:
22 import core.time;
24 import arsd.simpledisplay;
26 import iv.egra.gfx;
28 import iv.alice;
29 import iv.cmdcon;
30 import iv.dynstring;
31 import iv.strex;
32 import iv.utfutil;
34 import iv.egra.gui.subwindows;
37 // ////////////////////////////////////////////////////////////////////////// //
38 // this is used as parent if parent is null (but not for root widgets)
39 public Widget creatorCurrentParent = null;
42 // ////////////////////////////////////////////////////////////////////////// //
43 public enum WidgetStringPropertyMixin(string propname, string fieldName) = `
44 @property dynstring `~propname~` () const nothrow @safe @nogc { return `~fieldName~`; }
45 @property void `~propname~`(T:const(char)[]) (T v) nothrow @trusted {
46 static if (is(T == typeof(null))) {
47 `~fieldName~`.clear();
48 } else {
49 `~fieldName~` = v;
55 // ////////////////////////////////////////////////////////////////////////// //
56 public abstract class Widget : EgraStyledClass {
57 public:
58 static template isGoodEnterBoxDelegate(DG) {
59 import std.traits;
60 enum isGoodEnterBoxDelegate =
61 is(ReturnType!DG == void) &&
62 (is(typeof((inout int=0) { DG dg = void; Widget w; dg(w); })) ||
63 is(typeof((inout int=0) { DG dg = void; dg(); })));
66 static template isEnterBoxDelegateWithArgs(DG) {
67 enum isEnterBoxDelegateWithArgs =
68 isGoodEnterBoxDelegate!DG &&
69 is(typeof((inout int=0) { DG dg = void; Widget w; dg(w); }));
72 public:
73 Widget parent;
74 Widget firstChild;
75 Widget nextSibling;
77 GxRect rect; // relative to parent
78 bool tabStop;
79 // set this to `true` to skip painting; useful for spring and spacer widgets
80 // such widgets will not receive any events too (including sink/bubble)
81 // note that there is no reason to add children to such widgets, because
82 // non-visual widgets cannot be painted, and cannot be focused
83 bool nonVisual;
85 // this is for flexbox layouter
86 enum BoxHAlign {
87 Expand,
88 Left,
89 Center,
90 Right,
93 enum BoxVAlign {
94 Expand,
95 Top,
96 Center,
97 Bottom,
100 GxSize minSize; // minimum size
101 GxSize maxSize = GxSize(int.max, int.max); // maximum size
102 GxSize prefSize = GxSize(int.min, int.min); // preferred (initial) size; will be taken from widget size
104 GxSize boxsize; /// box size
105 GxSize finalSize; /// final size (can be bigger than `size)`
106 GxPoint finalPos; /// final position (for the final size); relative to the parent origin
108 GxDir childDir = GxDir.Horiz; /// children orientation
109 int flex; /// <=0: not flexible
111 BoxHAlign boxHAlign = BoxHAlign.Expand;
112 BoxVAlign boxVAlign = BoxVAlign.Expand;
114 // widgets with the same id will start with the equal preferred sizes (max of all)
115 string hsizeId;
116 string vsizeId;
118 Widget hsizeIdNext;
119 Widget vsizeIdNext;
121 public:
122 // for layouter
123 void preLayout () {
124 if (prefSize.w == int.min) prefSize = rect.size; else rect.size = prefSize;
127 void postLayout () {
128 rect.pos = finalPos;
129 rect.size = boxsize;
130 final switch (boxHAlign) with (BoxHAlign) {
131 case Expand: rect.size.w = finalSize.w; break;
132 case Left: break;
133 case Center: rect.pos.x = finalPos.x+(finalSize.w-boxsize.w)/2; break;
134 case Right: rect.pos.x = finalPos.x+finalSize.w-boxsize.w; break;
136 final switch (boxVAlign) with (BoxVAlign) {
137 case Expand: rect.size.h = finalSize.h; break;
138 case Top: break;
139 case Center: rect.pos.y = finalPos.y+(finalSize.h-boxsize.h)/2; break;
140 case Bottom: rect.pos.y = finalPos.y+finalSize.h-boxsize.h; break;
144 protected:
145 Widget mActive; // do not change directly!
146 dynstring mHotkey;
148 protected:
149 bool isMyHotkey (KeyEvent event) {
150 return
151 (mHotkey.length && event == mHotkey) ||
152 (onCheckHotkey !is null && onCheckHotkey(this, event));
155 // will be called if `isMyHotkey()` returned `true`
156 // return `true` if some action was done
157 bool hotkeyActivated () {
158 if (!canAcceptFocus) return false;
159 focus();
160 doAction();
161 return true;
164 public:
165 override bool isMyModifier (const(char)[] str) nothrow @trusted @nogc {
166 //if (nonVisual) return strEquCI("nonvisual");
167 if (str.length == 0) return true;
168 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: MOD: <%.*s>; focused=%d\n", cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(uint)str.length, str.ptr, cast(int)isFocused); }
169 if (strEquCI(str, "focused")) return isFocusedForStyle;
170 if (strEquCI(str, "unfocused")) return !isFocusedForStyle;
171 return false;
174 override EgraStyledClass getFirstChild () nothrow @trusted @nogc { return firstChild; }
175 override EgraStyledClass getNextSibling () nothrow @trusted @nogc { return nextSibling; }
176 override EgraStyledClass getParent () nothrow @trusted @nogc { return parent; }
178 override string getCurrentMod () nothrow @trusted @nogc {
179 return (isFocusedForStyle ? "focused" : "");
182 override void widgetChanged () nothrow {
183 EgraStyledClass w = getTopLevel();
184 if (w is null) return;
185 SubWindow win = cast(SubWindow)w;
186 if (win is null) return;
187 win.widgetChanged();
190 public:
191 dynstring getHotkey () { return mHotkey; }
192 void setHotkey (const(char)[] v) { mHotkey = v; }
194 bool delegate (Widget self, KeyEvent event) onCheckHotkey;
196 public:
197 void delegate (Widget self) onAction;
199 protected void doDefaultAction () {}
201 void doAction () {
202 if (onAction !is null) onAction(this); else doDefaultAction();
203 widgetChanged();
206 public:
207 static bool extractHotKey (ref dynstring text, ref dynstring hotch, ref int hotxpos, ref int hotxlen) nothrow @safe {
208 if (text.length < 2) return false;
209 auto umpos = text.indexOf('&');
210 if (umpos < 0 || umpos >= text.length-1) return false;
211 char ch = text[umpos+1];
212 if (ch > 32 && ch < 128) {
213 hotch = text[umpos+1..umpos+2];
214 text = text[0..umpos]~text[umpos+1..$];
215 } else if (ch >= 128) {
216 hotch = text[umpos+1..$];
217 while (hotch.utflen > 1) hotch = hotch.utfchop;
218 text = text[0..umpos]~text[umpos+1..$];
219 } else {
220 return false;
222 hotxpos = gxTextWidthUtf(text[0..umpos]);
223 hotxlen = gxTextWidthUtf(hotch);
224 hotch = "M-"~hotch;
225 return true;
228 public:
229 void addChild (Widget w) {
230 //{ import std.stdio; writefln("addChild(0x%08x); w=0x%08x", cast(uint)cast(void*)this, cast(uint)cast(void*)w); }
231 if (w is null) return;
232 assert (w !is this);
233 assert(w.parent is null);
234 if (cast(RootWidget)w) throw new Exception("root widget cannot be child of anything");
235 if (nonVisual) throw new Exception("non-visual widget cannot have children");
236 w.parent = this;
237 Widget lc = firstChild;
238 if (lc is null) {
239 firstChild = w;
240 } else {
241 while (lc.nextSibling) lc = lc.nextSibling;
242 lc.nextSibling = w;
244 w.invalidatePathCache();
245 w.widgetChanged();
246 widgetChanged();
247 //if (mActive is null && w.canAcceptFocus(w)) mActive = w;
250 Widget rootWidget () {
251 Widget res = this;
252 while (res.parent) res = res.parent;
253 return res;
256 static template isFEDGoodDelegate(DG) {
257 import std.traits;
258 enum isFEDGoodDelegate =
259 (is(ReturnType!DG == void) || is(ReturnType!DG == Widget)) &&
260 (is(typeof((inout int=0) { DG dg = void; Widget w; Widget res = dg(w); })) ||
261 is(typeof((inout int=0) { DG dg = void; Widget w; dg(w); })));
264 static template isFEDResDelegate(DG) {
265 import std.traits;
266 enum isFEDResDelegate = is(ReturnType!DG == Widget);
269 // Widget delegate (Widget w)
270 final Widget forEachDepth(DG) (scope DG dg) if (isFEDGoodDelegate!DG) {
271 for (Widget w = firstChild; w !is null; w = w.nextSibling) {
272 Widget res = w.forEachDepth(dg);
273 if (res !is null) return res;
275 static if (isFEDResDelegate!DG) {
276 return dg(this);
277 } else {
278 dg(this);
279 return null;
283 // Widget delegate (Widget w)
284 final Widget forEachVisualDepth(DG) (scope DG dg) if (isFEDGoodDelegate!DG) {
285 if (nonVisual) return null;
286 for (Widget w = firstChild; w !is null; w = w.nextSibling) {
287 if (!w.nonVisual) {
288 Widget res = w.forEachVisualDepth(dg);
289 if (res !is null) return res;
292 static if (isFEDResDelegate!DG) {
293 return dg(this);
294 } else {
295 dg(this);
296 return null;
300 static template isGoodIteratorDelegate(DG) {
301 import std.traits;
302 enum isGoodIteratorDelegate =
303 (is(ReturnType!DG == int)) &&
304 is(typeof((inout int=0) { DG dg = void; Widget w; int res = dg(w); }));
307 static struct IterChild {
308 Widget w;
309 bool onlyVisual;
311 this (Widget ww, in bool onlyvis) nothrow @safe @nogc {
312 pragma(inline, true);
313 w = ww;
314 onlyVisual = onlyvis;
317 int opApply(DG) (scope DG dg) if (isGoodIteratorDelegate!DG) {
318 int res = 0;
319 if (w is null || dg is null) return 0;
320 if (onlyVisual) {
321 w.forEachVisualDepth((Widget w) {
322 res = dg(w);
323 return (res ? w : null);
325 } else {
326 w.forEachDepth((Widget w) {
327 res = dg(w);
328 return (res ? w : null);
331 return res;
335 final IterChild allDepth () nothrow @safe @nogc { pragma(inline, true); return IterChild(this, false); }
336 final IterChild allVisualDepth () nothrow @safe @nogc { pragma(inline, true); return IterChild(this, true); }
337 final IterChild allDepth (in bool onlyvis) nothrow @safe @nogc { pragma(inline, true); return IterChild(this, onlyvis); }
339 public:
340 this () {
341 assert(creatorCurrentParent !is null);
342 assert(cast(RootWidget)this is null);
343 creatorCurrentParent.addChild(this);
346 this (Widget aparent) {
347 if (aparent !is null) aparent.addChild(this);
351 // box hierarchy
352 Widget enter(DG) (scope DG dg) if (isGoodEnterBoxDelegate!DG) {
353 if (dg !is null) {
354 Widget oldCCP = creatorCurrentParent;
355 creatorCurrentParent = this;
356 scope(exit) creatorCurrentParent = oldCCP;
357 static if (isEnterBoxDelegateWithArgs!DG) {
358 dg(this);
359 } else {
360 dg();
363 return this;
366 static Widget getCurrentBox () nothrow @safe @nogc {
367 pragma(inline, true);
368 return creatorCurrentParent;
372 // returns `true` if passed `this`
373 final bool isMyChild (in Widget w) pure const nothrow @safe @nogc {
374 pragma(inline, true);
375 return
376 w is null ? false :
377 w is this ? true :
378 isMyChild(w.parent);
381 // should be overriden in root widget
382 // should be called only for widgets without a parent
383 bool isOwnerFocused () nothrow @safe @nogc {
384 if (parent) return parent.isOwnerFocused();
385 return true;
388 // should be overriden in root widget
389 // should be called only for widgets without a parent
390 bool isOwnerInWindowList () nothrow @safe @nogc {
391 if (parent) return parent.isOwnerInWindowList();
392 return false;
395 // should be overriden in the root widget
396 void releaseGrab () {
399 // should be overriden in the root widget
400 GxPoint getGlobalOffset () nothrow @safe {
401 return GxPoint(0, 0);
404 // returns focused widget; it doesn't matter if root widget's owner is focused
405 // never returns `null`
406 @property Widget focusedWidget () nothrow @safe @nogc {
407 if (parent) return parent.focusedWidget;
408 Widget res = this;
409 while (res.mActive !is null) res = res.mActive;
410 return res;
413 // it doesn't matter if root widget's owner is focused
414 //FIXME: ask all parents too?
415 final @property bool focus () {
416 Widget oldFocus = focusedWidget;
417 if (oldFocus is this) return true;
418 if (!canAcceptFocus()) return false;
419 if (!oldFocus.canRemoveFocus()) return false;
420 widgetChanged();
421 releaseGrab();
422 oldFocus.onBlur();
423 Widget w = this;
424 while (w.parent !is null) {
425 w.parent.mActive = w;
426 w = w.parent;
428 onFocus();
429 return true;
432 // this returns `false` if root widget's parent is not focused
433 final @property bool isFocused () nothrow @safe @nogc {
434 pragma(inline, true);
435 return (isOwnerFocused() && this is focusedWidget);
438 // this ignores root widget's parent focus
439 final @property bool isFocusedForStyle () nothrow @safe @nogc {
440 pragma(inline, true);
441 return (this is focusedWidget && (isOwnerFocused || !isOwnerInWindowList));
444 // returns point translated to the topmost parent coords
445 // with proper root widget, returns global screen coordinates (useful for drawing)
446 final @property GxPoint point2Global (GxPoint pt) nothrow @safe {
447 if (parent is null) return pt+getGlobalOffset;
448 return parent.point2Global(pt+rect.pos);
451 // returns rectangle in topmost parent coords
452 // with proper root widget, returns global screen coordinates (useful for drawing)
453 final @property GxRect globalRect () nothrow @safe {
454 GxPoint pt = point2Global(GxPoint(0, 0));
455 return GxRect(pt, rect.size);
458 // this need to be called if you did automatic layouting, and then changed size or position
459 // it's not done automatically because it is rarely required
460 final void rectChanged () nothrow @safe @nogc {
461 pragma(inline, true);
462 prefSize = GxSize(int.min, int.min);
465 final @property ref inout(GxSize) size () inout pure nothrow @safe @nogc { pragma(inline, true); return rect.size; }
466 final @property void size (in GxSize sz) nothrow { pragma(inline, true); if (rect.size != sz) { rect.size = sz; widgetChanged(); } }
468 final @property ref inout(GxPoint) pos () inout pure nothrow @safe @nogc { pragma(inline, true); return rect.pos; }
469 final @property void pos (in GxPoint p) nothrow { pragma(inline, true); if (rect.pos != p) { rect.pos = p; widgetChanged(); } }
471 final @property int width () const pure nothrow @safe @nogc { pragma(inline, true); return rect.size.w; }
472 final @property int height () const pure nothrow @safe @nogc { pragma(inline, true); return rect.size.h; }
474 final @property void width (in int v) nothrow { pragma(inline, true); if (rect.size.w != v) { rect.size.w = v; widgetChanged(); } }
475 final @property void height (in int v) nothrow { pragma(inline, true); if (rect.size.h != v) { rect.size.h = v; widgetChanged(); } }
477 final @property int posx () const pure nothrow @safe @nogc { pragma(inline, true); return rect.pos.x; }
478 final @property int posy () const pure nothrow @safe @nogc { pragma(inline, true); return rect.pos.y; }
480 final @property void posx (in int v) nothrow { pragma(inline, true); if (rect.pos.x != v) { rect.pos.x = v; widgetChanged(); } }
481 final @property void posy (in int v) nothrow { pragma(inline, true); if (rect.pos.y != v) { rect.pos.y = v; widgetChanged(); } }
483 // this never returns `null`, even for out-of-bound coords
484 // for out-of-bounds case simply return `this`
485 final Widget childAt (GxPoint pt) pure nothrow @safe @nogc {
486 if (!pt.inside(rect.size)) return this;
487 Widget bestChild;
488 for (Widget w = firstChild; w !is null; w = w.nextSibling) {
489 if (!w.nonVisual && pt.inside(w.rect)) bestChild = w;
491 if (bestChild is null) return this;
492 pt -= bestChild.rect.pos;
493 return bestChild.childAt(pt);
496 final Widget childAt (in int x, in int y) pure nothrow @safe @nogc {
497 pragma(inline, true);
498 return childAt(GxPoint(x, y));
501 // this is called with set clip
502 // passed rectangle is global rect
503 // clip rect is set to the widget area
504 // `doPaint()` is called before painting children
505 // `doPaintPost()` is called after painting children
506 // it is safe to change clip rect there, it will be restored
507 // `grect` is in global screen coordinates
508 protected void doPaint (GxRect grect) {}
509 protected void doPaintPost (GxRect grect) {}
511 void onPaint () {
512 if (nonVisual || rect.empty) return; // nothing to paint here
513 if (gxClipRect.empty) return; // totally clipped away
514 gxWithSavedClip {
515 immutable GxRect grect = globalRect;
516 if (!gxClipRect.intersect(grect)) return;
517 // before-children painter
518 gxWithSavedClip { doPaint(grect); };
519 // paint children
520 for (Widget w = firstChild; w !is null; w = w.nextSibling) {
521 if (!w.nonVisual) gxWithSavedClip { w.onPaint(); };
523 // after-children painter
524 gxWithSavedClip { doPaintPost(grect); };
528 // return `false` to disable focus change
529 bool canRemoveFocus () { return true; }
531 // return `false` to disable focus change
532 bool canAcceptFocus () { return (!nonVisual && tabStop); }
534 // called before removing the focus
535 void onBlur () {}
537 // called after setting the focus
538 void onFocus () {}
540 // return `true` if the event was eaten
541 // coordinates are adjusted to the widget origin
542 bool onKey (KeyEvent event) { return false; }
543 bool onMouse (MouseEvent event) { return false; }
544 bool onChar (dchar ch) { return false; }
546 // coordinates are adjusted to the widget origin (NOT dest!)
547 bool onKeySink (Widget dest, KeyEvent event) { return false; }
548 bool onMouseSink (Widget dest, MouseEvent event) { return false; }
549 bool onCharSink (Widget dest, dchar ch) { return false; }
551 // coordinates are adjusted to the widget origin (NOT dest!)
552 bool onKeyBubble (Widget dest, KeyEvent event) { return false; }
553 bool onMouseBubble (Widget dest, MouseEvent event) { return false; }
554 bool onCharBubble (Widget dest, dchar ch) { return false; }
556 public:
557 // drawing utilities
558 // all coords are global
559 void drawTextCursor (int x, int y, int hgt=-666) {
560 if (hgt < 1) return;
562 auto ctt = (MonoTime.currTime.ticks*1000/MonoTime.ticksPerSecond)/100;
563 int doty = ctt%(hgt*2-1);
564 if (doty >= hgt) doty = hgt*2-doty-1;
565 immutable uint clr = getColor(ctt%10 < 5 ? "text-cursor-0" : "text-cursor-1");
567 immutable msecs = MonoTime.currTime.ticks*1000/MonoTime.ticksPerSecond;
568 immutable int btm = getInt("text-cursor-blink-time", 0);
569 immutable bool ctt = (btm >= 20 ? (msecs%btm < btm/2) : false);
570 immutable uint clr = getColor(ctt ? "text-cursor-0" : "text-cursor-1");
571 version(none) {
572 gxVLine(x, y, hgt, clr);
573 } else {
574 //gxFillRect(x, y, 2, hgt, clr);
575 gxFillRect(x, y+1, 2, hgt-2, clr);
576 gxHLine(x-2, y, 6, clr);
577 if (hgt > 1) gxHLine(x-2, y+hgt-1, 6, clr);
579 if (isFocused) {
580 int ctm = 0;
581 immutable uint clrDot = getColor("text-cursor-dot");
582 if (hgt > 2 && !gxIsTransparent(clrDot)) {
583 ctm = getInt("text-cursor-dot-time", 0);
584 if (ctm >= 20) {
585 hgt -= 2;
586 int doty = msecs/ctm%(hgt*2-1);
587 if (doty >= hgt) doty = hgt*2-doty-1;
588 version(none) {
589 gxPutPixel(x, y+doty+1, clrDot);
590 } else {
591 gxHLine(x, y+doty+1, 2, clrDot);
595 if (ctm >= 20 || btm >= 20) {
596 if (!ctm || ctm > (btm>>1)) ctm = (btm>>1);
597 postCurBlink(ctm);
604 // ////////////////////////////////////////////////////////////////////////// //
605 // this is root widget that is used for subwindow client area
606 public class RootWidget : Widget {
607 SubWindow owner;
608 uint mButtons; // used for grab
610 override EgraStyledClass getParent () nothrow @trusted @nogc { return owner; }
612 @disable this ();
614 this (SubWindow aowner) {
615 childDir = GxDir.Vert;
616 super(null);
617 owner = aowner;
618 if (aowner !is null) aowner.setRoot(this);
621 override bool isOwnerFocused () nothrow @safe @nogc {
622 if (owner is null) return false;
623 return owner.active;
626 override bool isOwnerInWindowList () nothrow @safe @nogc {
627 if (owner is null) return false;
628 return owner.inWindowList;
631 // can be called by the owner
632 // this also resets pressed mouse buttons
633 override void releaseGrab () {
634 mButtons = 0;
637 override GxPoint getGlobalOffset () nothrow @safe {
638 if (owner is null) return super.getGlobalOffset();
639 return GxPoint(owner.x0+owner.clientOffsetX, owner.y0+owner.clientOffsetY)+rect.pos;
642 void checkGrab () {
643 if (mButtons && !isOwnerFocused) releaseGrab();
646 bool hasGrab () {
647 checkGrab();
648 return (mButtons != 0);
651 enum EventPhase {
652 Sink,
653 Mine,
654 Bubble,
657 // dispatch event to this widget
658 // it implements sink/bubble model
659 // delegate is:
660 // bool delegate (Widget curr, Widget dest, EventPhase phase);
661 // returns "event eaten" flag (which stops propagation)
662 // `dest` must be a good child, and cannot be `null`
663 final bool dispatchTo(DG) (Widget dest, scope DG dg)
664 if (is(typeof((inout int=0) { DG dg = void; Widget w; EventPhase ph; immutable bool res = dg(w, w, ph); })))
666 // as we're walking from the bottom, first call it recursively, and then call delegate
667 bool sinkPhase() (Widget curr) {
668 if (curr is null) return false;
669 if (sinkPhase(curr.parent)) return true;
670 return dg(curr, dest, EventPhase.Sink);
673 if (dest is null || !isMyChild(dest)) return false;
675 if (sinkPhase(dest.parent)) return true;
676 if (dg(dest, dest, EventPhase.Mine)) return true;
677 for (Widget w = dest.parent; w !is null; w = w.parent) {
678 if (dg(w, dest, EventPhase.Bubble)) return true;
681 return false;
684 void doShiftTab () {
685 Widget pf = null;
686 Widget fc = focusedWidget;
687 foreach (Widget w; allVisualDepth) {
688 if (w is fc) break;
689 if (w.canAcceptFocus()) pf = w;
692 if (pf is null) {
693 // find last
694 foreach (Widget w; allVisualDepth) {
695 if (w.canAcceptFocus()) pf = w;
699 if (pf !is null) pf.focus();
702 void doTab () {
703 Widget fc = focusedWidget;
704 bool seenFC = (fc is null);
705 Widget nf = null;
706 foreach (Widget w; allVisualDepth) {
707 if (!seenFC) {
708 seenFC = (w is fc);
709 } else {
710 if (w.canAcceptFocus()) { nf = w; break; }
714 if (nf is null) {
715 // find first
716 foreach (Widget w; allVisualDepth) {
717 if (w.canAcceptFocus()) { nf = w; break; }
721 if (nf !is null) nf.focus();
724 override bool onKeyBubble (Widget dest, KeyEvent event) {
725 if (event.pressed) {
726 if (event == "Tab" || event == "C-Tab") { doTab(); return true; }
727 if (event == "S-Tab" || event == "C-S-Tab") { doShiftTab(); return true; }
729 Widget def;
730 immutable bool isDefAccept = (event == "Enter");
731 immutable bool isDefCancel = (event == "Escape");
733 // check hotkeys
734 Widget hk = null;
735 foreach (Widget w; allVisualDepth) {
736 if (w.isMyHotkey(event) && w.hotkeyActivated()) {
737 def = null;
738 hk = w;
739 break;
741 if (def is null) {
742 if (isDefAccept || isDefCancel) {
743 if (auto btn = cast(BaseButtonWidget)w) {
744 if ((isDefAccept && btn.deftype == BaseButtonWidget.Default.Accept) ||
745 (isDefCancel && btn.deftype == BaseButtonWidget.Default.Cancel))
747 if (btn.canAcceptFocus()) def = w;
754 if (hk !is null) return true;
755 if (def !is null && def.hotkeyActivated()) return true;
758 return super.onKeyBubble(dest, event);
761 bool dispatchKey (KeyEvent event) {
762 checkGrab();
763 return dispatchTo(focusedWidget, delegate bool (Widget curr, Widget dest, EventPhase phase) {
764 if (curr.nonVisual) return false;
765 final switch (phase) {
766 case EventPhase.Sink: return curr.onKeySink(dest, event);
767 case EventPhase.Mine: return curr.onKey(event);
768 case EventPhase.Bubble: return curr.onKeyBubble(dest, event);
770 assert(0); // just in case
774 // this is quite complicated...
775 protected Widget getMouseDestination (in MouseEvent event) {
776 checkGrab();
778 // still has a grab?
779 if (mButtons) {
780 // if some mouse buttons are still down, route everything to the focused widget
781 // also, release a grab here if we can (grab flag is not used anywhere else)
782 if (event.type == MouseEventType.buttonReleased) mButtons &= ~cast(uint)event.button;
783 return focusedWidget;
786 Widget dest = childAt(event.x, event.y);
787 assert(dest !is null);
789 // if mouse button is pressed, and there were no pressed buttons before,
790 // find the child, and check if it can grab events
791 if (event.type == MouseEventType.buttonPressed) {
792 if (dest !is focusedWidget) dest.focus();
793 if (dest is focusedWidget) {
794 // this activates the grab
795 mButtons = cast(uint)event.button;
796 } else {
797 releaseGrab();
799 } else {
800 // release grab, if necessary (it shouldn't be necessary here, but just in case...)
801 if (mButtons && event.type == MouseEventType.buttonReleased) {
802 mButtons &= ~cast(uint)event.button;
806 // route to the proper child
807 return dest;
810 // mouse event coords should be relative to our rect
811 bool dispatchMouse (MouseEvent event) {
812 Widget dest = getMouseDestination(event);
813 assert(dest !is null);
814 // convert event to global
815 immutable GxRect grect = globalRect;
816 event.x += grect.pos.x;
817 event.y += grect.pos.y;
818 return dispatchTo(dest, delegate bool (Widget curr, Widget dest, EventPhase phase) {
819 if (curr.nonVisual) return false;
820 // fix coordinates
821 immutable GxRect wrect = curr.globalRect;
822 MouseEvent ev = event;
823 ev.x -= wrect.pos.x;
824 ev.y -= wrect.pos.y;
825 final switch (phase) {
826 case EventPhase.Sink: return curr.onMouseSink(dest, ev);
827 case EventPhase.Mine: return curr.onMouse(ev);
828 case EventPhase.Bubble: return curr.onMouseBubble(dest, ev);
830 assert(0); // just in case
834 bool dispatchChar (dchar ch) {
835 checkGrab();
836 return dispatchTo(focusedWidget, delegate bool (Widget curr, Widget dest, EventPhase phase) {
837 if (curr.nonVisual) return false;
838 final switch (phase) {
839 case EventPhase.Sink: return curr.onCharSink(dest, ch);
840 case EventPhase.Mine: return curr.onChar(ch);
841 case EventPhase.Bubble: return curr.onCharBubble(dest, ch);
843 assert(0); // just in case
849 // ////////////////////////////////////////////////////////////////////////// //
850 public class SpacerWidget : Widget {
851 this (Widget aparent, in int asize) {
852 assert(aparent !is null);
853 super(aparent);
854 tabStop = false;
855 nonVisual = true;
856 if (aparent.childDir == GxDir.Horiz) rect.size.w = asize; else rect.size.h = asize;
859 this (in int asize) {
860 assert(creatorCurrentParent !is null);
861 this(creatorCurrentParent, asize);
865 public class SpringWidget : SpacerWidget {
866 this (Widget aparent, in int aflex) {
867 super(aparent, 0);
868 flex = aflex;
871 this (in int aflex) {
872 assert(creatorCurrentParent !is null);
873 this(creatorCurrentParent, aflex);
878 // ////////////////////////////////////////////////////////////////////////// //
879 public class BoxWidget : Widget {
880 this (Widget aparent) {
881 super(aparent);
882 tabStop = false;
885 this () {
886 assert(creatorCurrentParent !is null);
887 this(creatorCurrentParent);
891 public class HBoxWidget : BoxWidget {
892 this (Widget aparent) {
893 super(aparent);
894 childDir = GxDir.Horiz;
897 this () {
898 assert(creatorCurrentParent !is null);
899 this(creatorCurrentParent);
903 public class VBoxWidget : BoxWidget {
904 this (Widget aparent) {
905 super(aparent);
906 childDir = GxDir.Vert;
909 this () {
910 assert(creatorCurrentParent !is null);
911 this(creatorCurrentParent);
916 // ////////////////////////////////////////////////////////////////////////// //
917 public class LabelWidget : Widget {
918 public:
919 enum HAlign {
920 Left,
921 Center,
922 Right,
925 enum VAlign {
926 Top,
927 Center,
928 Bottom,
931 protected:
932 dynstring mText;
933 int hotxpos;
934 int hotxlen;
936 public:
937 HAlign halign = HAlign.Left;
938 VAlign valign = VAlign.Center;
939 int hpad = 0;
940 int vpad = 0;
942 public:
943 this(T:const(char)[]) (Widget aparent, T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) {
944 super(aparent);
945 text = atext;
946 rect.size.w = gxTextWidthUtf(mText);
947 rect.size.h = gxTextHeightUtf;
948 tabStop = false;
949 halign = horiz;
950 valign = vert;
953 this(T:const(char)[]) (T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) {
954 assert(creatorCurrentParent !is null);
955 this(creatorCurrentParent, atext, horiz, vert);
958 mixin(WidgetStringPropertyMixin!("text", "mText"));
960 protected void drawLabel (GxRect grect) {
961 if (mText.length == 0) return;
962 int x;
963 final switch (halign) {
964 case HAlign.Left: x = grect.x0+hpad; break;
965 case HAlign.Center: x = grect.x0+(grect.width-gxTextWidthUtf(mText))/2; break;
966 case HAlign.Right: x = grect.x0+grect.width-hpad-gxTextWidthUtf(mText); break;
968 int y;
969 final switch (valign) {
970 case VAlign.Top: y = grect.y0+vpad; break;
971 case VAlign.Center: y = grect.y0+(grect.height-gxTextHeightUtf)/2; break;
972 case VAlign.Bottom: y = grect.y0+grect.height-vpad-gxTextHeightUtf; break;
974 //gxDrawTextUtf(x0+(width-gxTextWidthUtf(mText))/2, y0+(height-gxTextHeightUtf)/2, mText, parent.clrWinText);
975 gxDrawTextUtf(x, y, mText, getColor("text"));
976 //gxDrawTextOutScaledUtf(1, x, y, mText, getColor("text"), gxRGB!(255, 0, 0));
977 //{ import core.stdc.stdio : printf; printf("LBL: 0x%08x\n", getColor("text")); }
978 if (hotxlen > 0) gxHLine(x+hotxpos, y+gxTextUnderLineUtf, hotxlen, getColor("hotline"));
981 protected override void doPaint (GxRect grect) {
982 gxFillRect(grect, getColor("back"));
983 drawLabel(grect);
988 // ////////////////////////////////////////////////////////////////////////// //
989 public class HotLabelWidget : LabelWidget {
990 public:
991 Widget dest; // if `null`, activate next focusable sibling
993 public:
994 this(T:const(char)[]) (Widget aparent, T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) {
995 super(aparent, atext, horiz, vert);
996 if (extractHotKey(ref mText, ref mHotkey, ref hotxpos, ref hotxlen)) rect.size.w = gxTextWidthUtf(mText);
999 this(T:const(char)[]) (T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) {
1000 assert(creatorCurrentParent !is null);
1001 this(creatorCurrentParent, atext, horiz, vert);
1004 // will be called if `isMyHotkey()` returned `true`
1005 // return `true` if some action was done
1006 override bool hotkeyActivated () {
1007 Widget d = dest;
1008 if (d is null && parent !is null) {
1009 bool seenSelf = false;
1010 Widget top = parent;
1011 while (top.parent !is null) top = top.parent;
1012 foreach (Widget w; top.allVisualDepth) {
1013 if (!seenSelf) {
1014 seenSelf = (w is this);
1015 } else {
1016 if (w.canAcceptFocus()) { d = w; break; }
1020 if (d is null || !d.canAcceptFocus()) return false;
1021 d.focus();
1022 return true;
1027 // ////////////////////////////////////////////////////////////////////////// //
1028 public class ProgressBarWidget : LabelWidget {
1029 protected:
1030 int mMin = 0;
1031 int mMax = 100;
1032 int mCurrent = 0;
1033 int lastWidth = int.min;
1034 int lastPxFull = int.min;
1036 private:
1037 final bool updateLast () {
1038 bool res = false;
1039 if (width != lastWidth) {
1040 lastWidth = width;
1041 res = true;
1043 if (lastWidth < 1) {
1044 if (lastPxFull != 0) {
1045 res = true;
1046 lastPxFull = 0;
1048 } else {
1049 int pxFull;
1050 if (mMin == mMax) {
1051 pxFull = lastWidth;
1052 } else {
1053 pxFull = cast(int)(cast(long)lastWidth*cast(long)(mCurrent-mMin)/cast(long)(mMax-mMin));
1055 if (pxFull != lastPxFull) {
1056 res = true;
1057 lastPxFull = pxFull;
1060 if (res) widgetChanged();
1061 return res;
1064 public:
1065 this(T:const(char)[]) (Widget aparent, T atext, HAlign horiz=HAlign.Center, VAlign vert=VAlign.Center) {
1066 super(aparent, atext, horiz, vert);
1067 height = height+4;
1070 this(T:const(char)[]) (T atext, HAlign horiz=HAlign.Center, VAlign vert=VAlign.Center) {
1071 assert(creatorCurrentParent !is null);
1072 this(creatorCurrentParent, atext, horiz, vert);
1075 final void setMinMax (int amin, int amax) {
1076 if (amin > amax) { immutable int tmp = amin; amin = amax; amax = tmp; }
1077 mMin = amin;
1078 mMax = amax;
1079 if (mCurrent < mMin) mCurrent = mMin;
1080 if (mCurrent > mMax) mCurrent = mMax;
1081 updateLast();
1084 final @property int current () const nothrow @safe @nogc {
1085 pragma(inline, true);
1086 return mCurrent;
1089 final @property void current (int val) {
1090 pragma(inline, true);
1091 if (val < mMin) val = mMin;
1092 if (val > mMax) val = mMax;
1093 mCurrent = val;
1094 updateLast();
1097 // returns `true` if need to repaint
1098 final bool setCurrentTotal (int val, int total) {
1099 if (total < 0) total = 0;
1100 setMinMax(0, total);
1101 if (val < 0) val = 0;
1102 if (val > total) val = total;
1103 mCurrent = val;
1104 return updateLast();
1107 protected void drawStripes (GxRect rect, in bool asfull) {
1108 if (rect.empty) return;
1110 immutable int sty = rect.pos.y;
1111 immutable int sth = rect.size.h;
1113 GxRect uprc = rect;
1114 if (rect.height > 4) uprc.size.h = 2;
1115 else if (rect.height > 2) uprc.size.h = 1;
1116 else uprc.size.h = 0;
1117 if (uprc.size.h > 0) {
1118 rect.pos.y += uprc.size.h;
1119 rect.size.h -= uprc.size.h;
1122 if (!uprc.empty) gxFillRect(uprc, getColor(asfull ? "back-full-hishade" : "back-hishade"));
1123 gxFillRect(rect, getColor(asfull ? "back-full" : "back"));
1125 immutable uint clrHi = getColor(asfull ? "stripe-full-hishade" : "stripe-hishade");
1126 immutable uint clrOk = getColor(asfull ? "stripe-full" : "stripe");
1128 foreach (int y0; 0..sth) {
1129 gxHStripedLine(rect.pos.x-y0, sty+y0, rect.size.w+y0, 16, (y0 < uprc.size.h ? clrHi : clrOk));
1133 protected override void doPaint (GxRect grect) {
1134 immutable uint clrRect = getColor("rect");
1136 gxDrawRect(grect, clrRect);
1137 gxClipRect.shrinkBy(1, 1);
1138 grect.shrinkBy(1, 1);
1139 if (grect.empty) return;
1141 if (lastWidth != width) updateLast();
1142 immutable int pxFull = lastPxFull;
1144 if (pxFull > 0) {
1145 gxWithSavedClip{
1146 GxRect rc = GxRect(grect.pos, pxFull, grect.height);
1147 if (gxClipRect.intersect(rc)) drawStripes(rc, asfull:true);
1151 if (pxFull < grect.width) {
1152 gxWithSavedClip{
1153 GxRect rc = grect;
1154 rc.pos.x += pxFull;
1155 if (gxClipRect.intersect(rc)) drawStripes(grect, asfull:false);
1159 if (grect.height > 2) grect.moveLeftTopBy(0, 1);
1160 drawLabel(grect);
1165 // ////////////////////////////////////////////////////////////////////////// //
1166 public abstract class BaseButtonWidget : Widget {
1167 public:
1168 enum Default {
1169 None,
1170 Accept,
1171 Cancel,
1174 protected:
1175 dynstring mTitle;
1176 int hotxpos;
1177 int hotxlen;
1179 public:
1180 Default deftype = Default.None;
1182 public:
1183 this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) {
1184 tabStop = true;
1185 super(aparent);
1186 title = atitle;
1187 hotkey = ahotkey;
1188 extractHotKey(ref mTitle, ref mHotkey, ref hotxpos, ref hotxlen);
1189 rect.size.w = gxTextWidthUtf(mTitle)+6;
1190 rect.size.h = gxTextHeightUtf+4;
1193 this(T0:const(char)[], T1:const(char)[]) (T0 atitle, T1 ahotkey=null) {
1194 assert(creatorCurrentParent !is null);
1195 this(creatorCurrentParent, atitle, ahotkey);
1198 mixin(WidgetStringPropertyMixin!("title", "mTitle"));
1199 mixin(WidgetStringPropertyMixin!("hotkey", "mHotkey"));
1201 override bool onKey (KeyEvent event) {
1202 if (!event.pressed || !isFocused) return super.onKey(event);
1203 if (event == "Enter" || event == "Space") {
1204 doAction();
1205 return true;
1207 return super.onKey(event);
1210 override bool onMouse (MouseEvent event) {
1211 if (!isFocused) return super.onMouse(event);
1212 if (GxPoint(event.x, event.y).inside(rect.size)) {
1213 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
1214 //{ import std.stdio; writeln("BTN(", mTitle, "): !!!"); }
1215 doAction();
1217 return true;
1219 return super.onMouse(event);
1224 // ////////////////////////////////////////////////////////////////////////// //
1225 public class ButtonWidget : BaseButtonWidget {
1226 public:
1227 this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) {
1228 super(aparent, atitle, ahotkey);
1231 this(T0:const(char)[], T1:const(char)[]) (T0 atitle, T1 ahotkey=null) {
1232 assert(creatorCurrentParent !is null);
1233 this(creatorCurrentParent, atitle, ahotkey);
1236 protected override void doPaint (GxRect grect) {
1237 immutable uint bclr = getColor("back");
1238 immutable uint fclr = getColor("text");
1239 gxFillRect(grect.x0+1, grect.y0+1, grect.width-2, grect.height-2, bclr);
1240 gxHLine(grect.x0+1, grect.y0+0, grect.width-2, bclr);
1241 gxHLine(grect.x0+1, grect.y1+0, grect.width-2, bclr);
1242 gxVLine(grect.x0+0, grect.y0+1, grect.height-2, bclr);
1243 gxVLine(grect.x1+0, grect.y0+1, grect.height-2, bclr);
1244 gxClipRect.shrinkBy(1, 1);
1245 immutable int tx = grect.x0+(width-gxTextWidthUtf(mTitle))/2;
1246 immutable int ty = grect.y0+(height-gxTextHeightUtf)/2;
1247 gxDrawTextUtf(tx, ty, mTitle, fclr);
1248 if (hotxlen > 0) gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, hotxlen, getColor("hotline"));
1253 // ////////////////////////////////////////////////////////////////////////// //
1254 public class ButtonExWidget : BaseButtonWidget {
1255 public:
1256 this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) {
1257 super(aparent, atitle, ahotkey);
1258 rect.size.h = rect.size.h+1;
1261 this(T0:const(char)[], T1:const(char)[]) (T0 atitle, T1 ahotkey=null) {
1262 assert(creatorCurrentParent !is null);
1263 this(creatorCurrentParent, atitle, ahotkey);
1266 protected override void doPaint (GxRect grect) {
1267 immutable uint bclr = getColor("back");
1268 immutable uint hclr = getColor("shadowline");
1269 immutable uint fclr = getColor("text");
1270 immutable uint brc = getColor("rect");
1272 gxFillRect(grect.x0+1, grect.y0+1, grect.width-2, grect.height-2, bclr);
1273 gxDrawRect(grect.x0, grect.y0, grect.width, grect.height, brc);
1274 gxHLine(grect.x0+1, grect.y0+1, grect.width-2, hclr);
1276 gxClipRect.shrinkBy(1, 1);
1277 immutable int tx = grect.x0+(width-gxTextWidthUtf(mTitle))/2;
1278 immutable int ty = grect.y0+(height-gxTextHeightUtf)/2;
1279 gxDrawTextUtf(tx, ty, mTitle, fclr);
1280 if (hotxlen > 0) gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, hotxlen, getColor("hotline"));
1285 // ////////////////////////////////////////////////////////////////////////// //
1286 public class CheckboxWidget : BaseButtonWidget {
1287 protected:
1288 bool mChecked;
1289 bool mDisabled;
1291 public:
1292 override bool isMyModifier (const(char)[] str) nothrow @trusted @nogc {
1293 if (mDisabled && str.length) return str.strEquCI("disabled");
1294 return super.isMyModifier(str);
1297 override string getCurrentMod () nothrow @trusted @nogc {
1298 if (mDisabled) return "disabled";
1299 return super.getCurrentMod();
1302 public:
1303 this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) {
1304 tabStop = true;
1305 super(aparent, atitle, ahotkey);
1306 width = gxTextWidthUtf(mTitle)+6+gxTextWidthUtf("[")+gxTextWidthUtf("x")+gxTextWidthUtf("]")+8;
1307 height = gxTextHeightUtf+4;
1310 this(T0:const(char)[], T1:const(char)[]) (T0 atitle, T1 ahotkey=null) {
1311 assert(creatorCurrentParent !is null);
1312 this(creatorCurrentParent, atitle, ahotkey);
1315 @property bool checked () const nothrow @safe @nogc { return mChecked; }
1316 @property void checked (in bool v) nothrow { if (mChecked != v) { mChecked = v; widgetChanged(); } }
1318 @property bool enabled () const nothrow @safe @nogc { return !mDisabled; }
1319 @property void enabled (in bool v) nothrow { if (mDisabled == v) { mDisabled = !v; tabStop = v; widgetChanged(); } }
1321 @property bool disabled () const nothrow @safe @nogc { return mDisabled; }
1322 @property void disabled (in bool v) nothrow { if (mDisabled != v) { mDisabled = v; tabStop = !v; widgetChanged(); } }
1324 override bool canAcceptFocus () { return (tabStop && !mDisabled); }
1326 protected override void doPaint (GxRect grect) {
1327 immutable uint fclr = getColor("text");
1328 immutable uint bclr = getColor("back");
1329 immutable uint xclr = getColor("mark");
1331 gxFillRect(grect, bclr);
1333 gxClipRect.shrinkBy(1, 1);
1334 int tx = grect.x0+3;
1335 immutable int ty = grect.y0+(grect.height-gxTextHeightUtf)/2;
1337 gxDrawTextUtf(tx, ty, "[", fclr);
1338 tx += gxTextWidthUtf("[");
1340 if (mChecked) gxDrawTextUtf(tx, ty, "x", xclr);
1341 tx += gxTextWidthUtf("x");
1343 gxDrawTextUtf(tx, ty, "]", fclr);
1344 tx += gxTextWidthUtf("]")+8;
1346 gxDrawTextUtf(tx, ty, mTitle, fclr);
1347 if (hotxlen > 0) gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, hotxlen, fclr);
1350 protected override void doDefaultAction () {
1351 mChecked = !mChecked;
1356 // ////////////////////////////////////////////////////////////////////////// //
1357 public final class SimpleListBoxWidget : Widget {
1358 private:
1359 dynstring[] mItems;
1360 Object[] mItemData;
1361 int mTopIdx;
1362 int mCurIdx;
1364 public:
1365 this (Widget aparent) {
1366 super(aparent);
1369 this () {
1370 assert(creatorCurrentParent !is null);
1371 this(creatorCurrentParent);
1374 @property int curidx () const nothrow @safe @nogc { return (mCurIdx >= 0 && mCurIdx < mItems.length ? mCurIdx : 0); }
1376 @property void curidx (int idx) nothrow {
1377 if (mItems.length == 0) return;
1378 if (idx < 0) idx = 0;
1379 if (idx >= mItems.length) idx = cast(int)(mItems.length-1);
1380 if (mCurIdx != idx) {
1381 mCurIdx = idx;
1382 widgetChanged();
1386 @property int length () const nothrow @safe @nogc { return cast(int)mItems.length; }
1387 @property dynstring opIndex (usize idx) const nothrow @safe @nogc { return (idx < mItems.length ? mItems[idx] : dynstring()); }
1388 @property Object itemData (usize idx) nothrow @safe @nogc { return (idx < mItems.length ? mItemData[idx] : null); }
1390 void appendItem(T:const(char)[]) (T s, Object o=null) {
1391 //conwriteln("new item: ", s);
1392 dynstring it = s;
1393 mItems ~= it;
1394 mItemData ~= o;
1395 widgetChanged();
1398 @property int visibleItemsCount () const nothrow @trusted {
1399 pragma(inline, true);
1400 return (height < gxTextHeightUtf*2 ? 1 : height/gxTextHeightUtf);
1403 void makeCursorVisible (bool needupdate=true) {
1404 if (mItems.length == 0) return;
1405 immutable int omci = mCurIdx;
1406 scope(exit) if (needupdate && mCurIdx != omci) widgetChanged();
1407 if (mCurIdx < 0) mCurIdx = 0;
1408 if (mCurIdx >= mItems.length) mCurIdx = cast(int)(mItems.length-1);
1409 if (mCurIdx < mTopIdx) { mTopIdx = mCurIdx; return; }
1410 int icnt = visibleItemsCount-1;
1411 if (mTopIdx+icnt < mCurIdx) mTopIdx = mCurIdx-icnt;
1414 protected override void doPaint (GxRect grect) {
1415 makeCursorVisible(needupdate:false);
1416 uint bclr = getColor("back");
1417 uint tclr = getColor("text");
1418 uint cbclr = getColor("cursor-back");
1419 uint ctclr = getColor("cursor-text");
1420 gxFillRect(grect, bclr);
1421 int y = 0;
1422 int idx = mTopIdx;
1423 while (idx < mItems.length && y < grect.height) {
1424 if (idx >= 0) {
1425 uint clr = tclr;
1426 if (idx == mCurIdx) {
1427 gxFillRect(grect.x0, grect.y0+y, grect.width, gxTextHeightUtf, cbclr);
1428 clr = ctclr;
1430 gxWithSavedClip {
1431 gxClipRect.intersect(GxRect(grect.pos, GxPoint(grect.x1-1, grect.y1)));
1432 gxDrawTextUtf(grect.x0+1, grect.y0+y, mItems[idx], clr);
1435 ++idx;
1436 y += gxTextHeightUtf;
1440 override bool onKey (KeyEvent event) {
1441 if (!event.pressed || !isFocused) return super.onKey(event);
1442 if (event == "Up") { curidx = curidx-1; return true; }
1443 if (event == "Down") { curidx = curidx+1; return true; }
1444 if (event == "Home") { curidx = 0; return true; }
1445 if (event == "End") { curidx = cast(int)(mItems.length-1); return true; }
1446 if (event == "PageUp") {
1447 makeCursorVisible();
1448 if (curidx > mTopIdx) {
1449 curidx = mTopIdx;
1450 } else {
1451 curidx = curidx-(visibleItemsCount-1);
1453 return true;
1455 if (event == "PageDown") {
1456 makeCursorVisible();
1457 int icnt = visibleItemsCount-1;
1458 if (icnt) {
1459 if (mTopIdx+icnt < curidx) {
1460 curidx = mTopIdx+icnt;
1461 } else {
1462 curidx = curidx+icnt;
1465 return true;
1467 return super.onKey(event);
1470 override bool onMouse (MouseEvent event) {
1471 if (!isFocused) return super.onMouse(event);
1472 if (GxPoint(event.x, event.y).inside(rect.size)) {
1473 int mx = event.x, my = event.y;
1474 makeCursorVisible();
1475 immutable int idx = mTopIdx+my/gxTextHeightUtf;
1476 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1477 if (curidx == idx) doAction(); else curidx = idx;
1478 } else if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelUp) {
1479 curidx = curidx-1;
1480 } else if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelDown) {
1481 curidx = curidx+1;
1483 return true;
1485 return super.onMouse(event);