erga: switched to css-like styles (not fully debugged yet)
[iv.d.git] / egra / gui / widgets.d
blob76e21508bf9eab0a96b3f7eff46f905f1d47540e
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 postScreenRebuild();
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 //if (mActive is null && w.canAcceptFocus(w)) mActive = w;
247 Widget rootWidget () {
248 Widget res = this;
249 while (res.parent) res = res.parent;
250 return res;
253 static template isFEDGoodDelegate(DG) {
254 import std.traits;
255 enum isFEDGoodDelegate =
256 (is(ReturnType!DG == void) || is(ReturnType!DG == Widget)) &&
257 (is(typeof((inout int=0) { DG dg = void; Widget w; Widget res = dg(w); })) ||
258 is(typeof((inout int=0) { DG dg = void; Widget w; dg(w); })));
261 static template isFEDResDelegate(DG) {
262 import std.traits;
263 enum isFEDResDelegate = is(ReturnType!DG == Widget);
266 // Widget delegate (Widget w)
267 final Widget forEachDepth(DG) (scope DG dg) if (isFEDGoodDelegate!DG) {
268 for (Widget w = firstChild; w !is null; w = w.nextSibling) {
269 Widget res = w.forEachDepth(dg);
270 if (res !is null) return res;
272 static if (isFEDResDelegate!DG) {
273 return dg(this);
274 } else {
275 dg(this);
276 return null;
280 // Widget delegate (Widget w)
281 final Widget forEachVisualDepth(DG) (scope DG dg) if (isFEDGoodDelegate!DG) {
282 if (nonVisual) return null;
283 for (Widget w = firstChild; w !is null; w = w.nextSibling) {
284 if (!w.nonVisual) {
285 Widget res = w.forEachVisualDepth(dg);
286 if (res !is null) return res;
289 static if (isFEDResDelegate!DG) {
290 return dg(this);
291 } else {
292 dg(this);
293 return null;
297 public:
298 this () {
299 assert(creatorCurrentParent !is null);
300 assert(cast(RootWidget)this is null);
301 creatorCurrentParent.addChild(this);
304 this (Widget aparent) {
305 if (aparent !is null) aparent.addChild(this);
309 // box hierarchy
310 Widget enter(DG) (scope DG dg) if (isGoodEnterBoxDelegate!DG) {
311 if (dg !is null) {
312 Widget oldCCP = creatorCurrentParent;
313 creatorCurrentParent = this;
314 scope(exit) creatorCurrentParent = oldCCP;
315 static if (isEnterBoxDelegateWithArgs!DG) {
316 dg(this);
317 } else {
318 dg();
321 return this;
324 static Widget getCurrentBox () nothrow @safe @nogc {
325 pragma(inline, true);
326 return creatorCurrentParent;
330 // returns `true` if passed `this`
331 final bool isMyChild (in Widget w) pure const nothrow @safe @nogc {
332 pragma(inline, true);
333 return
334 w is null ? false :
335 w is this ? true :
336 isMyChild(w.parent);
339 // should be overriden in root widget
340 // should be called only for widgets without a parent
341 bool isOwnerFocused () nothrow @safe @nogc {
342 if (parent) return parent.isOwnerFocused();
343 return true;
346 // should be overriden in root widget
347 // should be called only for widgets without a parent
348 bool isOwnerInWindowList () nothrow @safe @nogc {
349 if (parent) return parent.isOwnerInWindowList();
350 return false;
353 // should be overriden in the root widget
354 void releaseGrab () {
357 // should be overriden in the root widget
358 GxPoint getGlobalOffset () nothrow @safe {
359 return GxPoint(0, 0);
362 // returns focused widget; it doesn't matter if root widget's owner is focused
363 // never returns `null`
364 @property Widget focusedWidget () nothrow @safe @nogc {
365 if (parent) return parent.focusedWidget;
366 Widget res = this;
367 while (res.mActive !is null) res = res.mActive;
368 return res;
371 // it doesn't matter if root widget's owner is focused
372 //FIXME: ask all parents too?
373 final @property bool focus () {
374 Widget oldFocus = focusedWidget;
375 if (oldFocus is this) return true;
376 if (!canAcceptFocus()) return false;
377 if (!oldFocus.canRemoveFocus()) return false;
378 postScreenRebuild();
379 releaseGrab();
380 oldFocus.onBlur();
381 Widget w = this;
382 while (w.parent !is null) {
383 w.parent.mActive = w;
384 w = w.parent;
386 onFocus();
387 return true;
390 // this returns `false` if root widget's parent is not focused
391 final @property bool isFocused () nothrow @safe @nogc {
392 pragma(inline, true);
393 return (isOwnerFocused() && this is focusedWidget);
396 // this ignores root widget's parent focus
397 final @property bool isFocusedForStyle () nothrow @safe @nogc {
398 pragma(inline, true);
399 return (this is focusedWidget && (isOwnerFocused || !isOwnerInWindowList));
402 // returns point translated to the topmost parent coords
403 // with proper root widget, returns global screen coordinates (useful for drawing)
404 final @property GxPoint point2Global (GxPoint pt) nothrow @safe {
405 if (parent is null) return pt+getGlobalOffset;
406 return parent.point2Global(pt+rect.pos);
409 // returns rectangle in topmost parent coords
410 // with proper root widget, returns global screen coordinates (useful for drawing)
411 final @property GxRect globalRect () nothrow @safe {
412 GxPoint pt = point2Global(GxPoint(0, 0));
413 return GxRect(pt, rect.size);
416 // this need to be called if you did automatic layouting, and then changed size or position
417 // it's not done automatically because it is rarely required
418 final void rectChanged () nothrow @safe @nogc {
419 pragma(inline, true);
420 prefSize = GxSize(int.min, int.min);
423 final @property ref inout(GxSize) size () inout pure nothrow @safe @nogc { pragma(inline, true); return rect.size; }
424 final @property void size (in GxSize sz) nothrow @safe @nogc { pragma(inline, true); rect.size = sz; }
426 final @property ref inout(GxPoint) pos () inout pure nothrow @safe @nogc { pragma(inline, true); return rect.pos; }
427 final @property void pos (in GxPoint p) nothrow @safe @nogc { pragma(inline, true); rect.pos = p; }
429 final @property int width () const pure nothrow @safe @nogc { pragma(inline, true); return rect.size.w; }
430 final @property int height () const pure nothrow @safe @nogc { pragma(inline, true); return rect.size.h; }
432 final @property void width (in int v) nothrow @safe @nogc { pragma(inline, true); rect.size.w = v; }
433 final @property void height (in int v) nothrow @safe @nogc { pragma(inline, true); rect.size.h = v; }
435 final @property int posx () const pure nothrow @safe @nogc { pragma(inline, true); return rect.pos.x; }
436 final @property int posy () const pure nothrow @safe @nogc { pragma(inline, true); return rect.pos.y; }
438 final @property void posx (in int v) nothrow @safe @nogc { pragma(inline, true); rect.pos.x = v; }
439 final @property void posy (in int v) nothrow @safe @nogc { pragma(inline, true); rect.pos.y = v; }
441 // this never returns `null`, even for out-of-bound coords
442 // for out-of-bounds case simply return `this`
443 final Widget childAt (GxPoint pt) pure nothrow @safe @nogc {
444 if (!pt.inside(rect.size)) return this;
445 Widget bestChild;
446 for (Widget w = firstChild; w !is null; w = w.nextSibling) {
447 if (!w.nonVisual && pt.inside(w.rect)) bestChild = w;
449 if (bestChild is null) return this;
450 pt -= bestChild.rect.pos;
451 return bestChild.childAt(pt);
454 final Widget childAt (in int x, in int y) pure nothrow @safe @nogc {
455 pragma(inline, true);
456 return childAt(GxPoint(x, y));
459 // this is called with set clip
460 // passed rectangle is global rect
461 // clip rect is set to the widget area
462 // `doPaint()` is called before painting children
463 // `doPaintPost()` is called after painting children
464 // it is safe to change clip rect there, it will be restored
465 // `grect` is in global screen coordinates
466 protected void doPaint (GxRect grect) {}
467 protected void doPaintPost (GxRect grect) {}
469 void onPaint () {
470 if (nonVisual || rect.empty) return; // nothing to paint here
471 if (gxClipRect.empty) return; // totally clipped away
472 gxWithSavedClip {
473 immutable GxRect grect = globalRect;
474 if (!gxClipRect.intersect(grect)) return;
475 // before-children painter
476 gxWithSavedClip { doPaint(grect); };
477 // paint children
478 for (Widget w = firstChild; w !is null; w = w.nextSibling) {
479 if (!w.nonVisual) gxWithSavedClip { w.onPaint(); };
481 // after-children painter
482 gxWithSavedClip { doPaintPost(grect); };
486 // return `false` to disable focus change
487 bool canRemoveFocus () { return true; }
489 // return `false` to disable focus change
490 bool canAcceptFocus () { return (!nonVisual && tabStop); }
492 // called before removing the focus
493 void onBlur () {}
495 // called after setting the focus
496 void onFocus () {}
498 // return `true` if the event was eaten
499 // coordinates are adjusted to the widget origin
500 bool onKey (KeyEvent event) { return false; }
501 bool onMouse (MouseEvent event) { return false; }
502 bool onChar (dchar ch) { return false; }
504 // coordinates are adjusted to the widget origin (NOT dest!)
505 bool onKeySink (Widget dest, KeyEvent event) { return false; }
506 bool onMouseSink (Widget dest, MouseEvent event) { return false; }
507 bool onCharSink (Widget dest, dchar ch) { return false; }
509 // coordinates are adjusted to the widget origin (NOT dest!)
510 bool onKeyBubble (Widget dest, KeyEvent event) { return false; }
511 bool onMouseBubble (Widget dest, MouseEvent event) { return false; }
512 bool onCharBubble (Widget dest, dchar ch) { return false; }
514 public:
515 // drawing utilities
516 // all coords are global
517 void drawTextCursor (int x, int y, int hgt=-666) {
518 if (hgt < 1) return;
520 auto ctt = (MonoTime.currTime.ticks*1000/MonoTime.ticksPerSecond)/100;
521 int doty = ctt%(hgt*2-1);
522 if (doty >= hgt) doty = hgt*2-doty-1;
523 immutable uint clr = getColor(ctt%10 < 5 ? "text-cursor-0" : "text-cursor-1");
525 immutable msecs = MonoTime.currTime.ticks*1000/MonoTime.ticksPerSecond;
526 immutable int btm = getInt("text-cursor-blink-time", 0);
527 int ctm = 0;
528 bool ctt = (btm >= 10 ? (msecs%btm < btm/2) : false);
529 immutable uint clr = getColor(ctt ? "text-cursor-0" : "text-cursor-1");
530 version(none) {
531 gxVLine(x, y, hgt, clr);
532 } else {
533 //gxFillRect(x, y, 2, hgt, clr);
534 gxFillRect(x, y+1, 2, hgt-2, clr);
535 gxHLine(x-2, y, 6, clr);
536 if (hgt > 1) gxHLine(x-2, y+hgt-1, 6, clr);
538 if (isFocused) {
539 if (hgt > 2) {
540 ctm = getInt("text-cursor-dot-time", 0);
541 if (ctm >= 10) {
542 hgt -= 2;
543 int doty = msecs/ctm%(hgt*2-1);
544 if (doty >= hgt) doty = hgt*2-doty-1;
545 immutable uint clrDot = getColor("text-cursor-dot");
546 version(none) {
547 gxPutPixel(x, y+doty+1, clrDot);
548 } else {
549 gxHLine(x, y+doty+1, 2, clrDot);
553 if (ctm >= 10 || btm >= 10) {
554 if (!ctm || ctm > btm) ctm = btm;
555 postCurBlink(ctm);
562 // ////////////////////////////////////////////////////////////////////////// //
563 // this is root widget that is used for subwindow client area
564 public class RootWidget : Widget {
565 SubWindow owner;
566 uint mButtons; // used for grab
568 override EgraStyledClass getParent () nothrow @trusted @nogc { return owner; }
570 @disable this ();
572 this (SubWindow aowner) {
573 childDir = GxDir.Vert;
574 super(null);
575 owner = aowner;
576 if (aowner !is null) aowner.setRoot(this);
579 override bool isOwnerFocused () nothrow @safe @nogc {
580 if (owner is null) return false;
581 return owner.active;
584 override bool isOwnerInWindowList () nothrow @safe @nogc {
585 if (owner is null) return false;
586 return owner.inWindowList;
589 // can be called by the owner
590 // this also resets pressed mouse buttons
591 override void releaseGrab () {
592 mButtons = 0;
595 override GxPoint getGlobalOffset () nothrow @safe {
596 if (owner is null) return super.getGlobalOffset();
597 return GxPoint(owner.x0+owner.clientOffsetX, owner.y0+owner.clientOffsetY)+rect.pos;
600 void checkGrab () {
601 if (mButtons && !isOwnerFocused) releaseGrab();
604 bool hasGrab () {
605 checkGrab();
606 return (mButtons != 0);
609 enum EventPhase {
610 Sink,
611 Mine,
612 Bubble,
615 // dispatch event to this widget
616 // it implements sink/bubble model
617 // delegate is:
618 // bool delegate (Widget curr, Widget dest, EventPhase phase);
619 // returns "event eaten" flag (which stops propagation)
620 // `dest` must be a good child, and cannot be `null`
621 final bool dispatchTo(DG) (Widget dest, scope DG dg)
622 if (is(typeof((inout int=0) { DG dg = void; Widget w; EventPhase ph; immutable bool res = dg(w, w, ph); })))
624 // as we're walking from the bottom, first call it recursively, and then call delegate
625 bool sinkPhase() (Widget curr) {
626 if (curr is null) return false;
627 if (sinkPhase(curr.parent)) return true;
628 return dg(curr, dest, EventPhase.Sink);
631 if (dest is null || !isMyChild(dest)) return false;
633 if (sinkPhase(dest.parent)) return true;
634 if (dg(dest, dest, EventPhase.Mine)) return true;
635 for (Widget w = dest.parent; w !is null; w = w.parent) {
636 if (dg(w, dest, EventPhase.Bubble)) return true;
639 return false;
642 void doShiftTab () {
643 Widget pf = null;
644 Widget fc = focusedWidget;
645 forEachVisualDepth(delegate Widget (Widget w) {
646 if (w is fc) return w;
647 if (w.canAcceptFocus()) {
648 pf = w;
649 if (fc is null) return w;
651 return null;
654 if (pf is null) {
655 // find last
656 forEachVisualDepth((Widget w) {
657 if (w.canAcceptFocus()) pf = w;
661 if (pf !is null) pf.focus();
664 void doTab () {
665 Widget fc = focusedWidget;
666 bool seenFC = (fc is null);
667 Widget nf = forEachVisualDepth(delegate Widget (Widget w) {
668 if (!seenFC) {
669 seenFC = (w is fc);
670 } else {
671 if (w.canAcceptFocus()) return w;
673 return null;
676 if (nf is null) {
677 // find first
678 nf = forEachVisualDepth(delegate Widget (Widget w) {
679 if (w.canAcceptFocus()) return w;
680 return null;
684 if (nf !is null) nf.focus();
687 override bool onKeyBubble (Widget dest, KeyEvent event) {
688 if (event.pressed) {
689 if (event == "Tab" || event == "C-Tab") { doTab(); return true; }
690 if (event == "S-Tab" || event == "C-S-Tab") { doShiftTab(); return true; }
692 Widget def;
693 immutable bool isDefAccept = (event == "Enter");
694 immutable bool isDefCancel = (event == "Escape");
696 // check hotkeys
697 Widget hk = forEachVisualDepth(delegate Widget (Widget w) {
698 if (w.isMyHotkey(event) && w.hotkeyActivated()) {
699 def = null;
700 return w;
702 if (def is null) {
703 if (isDefAccept || isDefCancel) {
704 if (auto btn = cast(ButtonWidget)w) {
705 if ((isDefAccept && btn.deftype == ButtonWidget.Default.Accept) ||
706 (isDefCancel && btn.deftype == ButtonWidget.Default.Cancel))
708 if (btn.canAcceptFocus()) def = w;
713 return null;
716 if (hk !is null) return true;
717 if (def !is null && def.hotkeyActivated()) return true;
720 return super.onKeyBubble(dest, event);
723 bool dispatchKey (KeyEvent event) {
724 checkGrab();
725 return dispatchTo(focusedWidget, delegate bool (Widget curr, Widget dest, EventPhase phase) {
726 if (curr.nonVisual) return false;
727 final switch (phase) {
728 case EventPhase.Sink: return curr.onKeySink(dest, event);
729 case EventPhase.Mine: return curr.onKey(event);
730 case EventPhase.Bubble: return curr.onKeyBubble(dest, event);
732 assert(0); // just in case
736 // this is quite complicated...
737 protected Widget getMouseDestination (in MouseEvent event) {
738 checkGrab();
740 // still has a grab?
741 if (mButtons) {
742 // if some mouse buttons are still down, route everything to the focused widget
743 // also, release a grab here if we can (grab flag is not used anywhere else)
744 if (event.type == MouseEventType.buttonReleased) mButtons &= ~cast(uint)event.button;
745 return focusedWidget;
748 Widget dest = childAt(event.x, event.y);
749 assert(dest !is null);
751 // if mouse button is pressed, and there were no pressed buttons before,
752 // find the child, and check if it can grab events
753 if (event.type == MouseEventType.buttonPressed) {
754 if (dest !is focusedWidget) dest.focus();
755 if (dest is focusedWidget) {
756 // this activates the grab
757 mButtons = cast(uint)event.button;
758 } else {
759 releaseGrab();
761 } else {
762 // release grab, if necessary (it shouldn't be necessary here, but just in case...)
763 if (mButtons && event.type == MouseEventType.buttonReleased) {
764 mButtons &= ~cast(uint)event.button;
768 // route to the proper child
769 return dest;
772 // mouse event coords should be relative to our rect
773 bool dispatchMouse (MouseEvent event) {
774 Widget dest = getMouseDestination(event);
775 assert(dest !is null);
776 // convert event to global
777 immutable GxRect grect = globalRect;
778 event.x += grect.pos.x;
779 event.y += grect.pos.y;
780 return dispatchTo(dest, delegate bool (Widget curr, Widget dest, EventPhase phase) {
781 if (curr.nonVisual) return false;
782 // fix coordinates
783 immutable GxRect wrect = curr.globalRect;
784 MouseEvent ev = event;
785 ev.x -= wrect.pos.x;
786 ev.y -= wrect.pos.y;
787 final switch (phase) {
788 case EventPhase.Sink: return curr.onMouseSink(dest, ev);
789 case EventPhase.Mine: return curr.onMouse(ev);
790 case EventPhase.Bubble: return curr.onMouseBubble(dest, ev);
792 assert(0); // just in case
796 bool dispatchChar (dchar ch) {
797 checkGrab();
798 return dispatchTo(focusedWidget, delegate bool (Widget curr, Widget dest, EventPhase phase) {
799 if (curr.nonVisual) return false;
800 final switch (phase) {
801 case EventPhase.Sink: return curr.onCharSink(dest, ch);
802 case EventPhase.Mine: return curr.onChar(ch);
803 case EventPhase.Bubble: return curr.onCharBubble(dest, ch);
805 assert(0); // just in case
811 // ////////////////////////////////////////////////////////////////////////// //
812 public class SpacerWidget : Widget {
813 this (Widget aparent, in int asize) {
814 assert(aparent !is null);
815 super(aparent);
816 tabStop = false;
817 nonVisual = true;
818 if (aparent.childDir == GxDir.Horiz) rect.size.w = asize; else rect.size.h = asize;
821 this (in int asize) {
822 assert(creatorCurrentParent !is null);
823 this(creatorCurrentParent, asize);
827 public class SpringWidget : SpacerWidget {
828 this (Widget aparent, in int aflex) {
829 super(aparent, 0);
830 flex = aflex;
833 this (in int aflex) {
834 assert(creatorCurrentParent !is null);
835 this(creatorCurrentParent, aflex);
840 // ////////////////////////////////////////////////////////////////////////// //
841 public class BoxWidget : Widget {
842 this (Widget aparent) {
843 super(aparent);
844 tabStop = false;
847 this () {
848 assert(creatorCurrentParent !is null);
849 this(creatorCurrentParent);
853 public class HBoxWidget : BoxWidget {
854 this (Widget aparent) {
855 super(aparent);
856 childDir = GxDir.Horiz;
859 this () {
860 assert(creatorCurrentParent !is null);
861 this(creatorCurrentParent);
865 public class VBoxWidget : BoxWidget {
866 this (Widget aparent) {
867 super(aparent);
868 childDir = GxDir.Vert;
871 this () {
872 assert(creatorCurrentParent !is null);
873 this(creatorCurrentParent);
878 // ////////////////////////////////////////////////////////////////////////// //
879 public class LabelWidget : Widget {
880 public:
881 enum HAlign {
882 Left,
883 Center,
884 Right,
887 enum VAlign {
888 Top,
889 Center,
890 Bottom,
893 protected:
894 dynstring mText;
895 int hotxpos;
896 int hotxlen;
898 public:
899 HAlign halign = HAlign.Left;
900 VAlign valign = VAlign.Center;
901 int hpad = 0;
902 int vpad = 0;
904 public:
905 this(T:const(char)[]) (Widget aparent, T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) {
906 super(aparent);
907 text = atext;
908 rect.size.w = gxTextWidthUtf(mText);
909 rect.size.h = gxTextHeightUtf;
910 tabStop = false;
911 halign = horiz;
912 valign = vert;
915 this(T:const(char)[]) (T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) {
916 assert(creatorCurrentParent !is null);
917 this(creatorCurrentParent, atext, horiz, vert);
920 mixin(WidgetStringPropertyMixin!("text", "mText"));
922 protected void drawLabel (GxRect grect) {
923 if (mText.length == 0) return;
924 int x;
925 final switch (halign) {
926 case HAlign.Left: x = grect.x0+hpad; break;
927 case HAlign.Center: x = grect.x0+(grect.width-gxTextWidthUtf(mText))/2; break;
928 case HAlign.Right: x = grect.x0+grect.width-hpad-gxTextWidthUtf(mText); break;
930 int y;
931 final switch (valign) {
932 case VAlign.Top: y = grect.y0+vpad; break;
933 case VAlign.Center: y = grect.y0+(grect.height-gxTextHeightUtf)/2; break;
934 case VAlign.Bottom: y = grect.y0+grect.height-vpad-gxTextHeightUtf; break;
936 //gxDrawTextUtf(x0+(width-gxTextWidthUtf(mText))/2, y0+(height-gxTextHeightUtf)/2, mText, parent.clrWinText);
937 gxDrawTextUtf(x, y, mText, getColor("text"));
938 //gxDrawTextOutScaledUtf(1, x, y, mText, getColor("text"), gxRGB!(255, 0, 0));
939 //{ import core.stdc.stdio : printf; printf("LBL: 0x%08x\n", getColor("text")); }
940 if (hotxlen > 0) gxHLine(x+hotxpos, y+gxTextUnderLineUtf, hotxlen, getColor("hotline"));
943 protected override void doPaint (GxRect grect) {
944 gxFillRect(grect, getColor("back"));
945 drawLabel(grect);
950 // ////////////////////////////////////////////////////////////////////////// //
951 public class HotLabelWidget : LabelWidget {
952 public:
953 Widget dest; // if `null`, activate next focusable sibling
955 public:
956 this(T:const(char)[]) (Widget aparent, T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) {
957 super(aparent, atext, horiz, vert);
958 if (extractHotKey(ref mText, ref mHotkey, ref hotxpos, ref hotxlen)) rect.size.w = gxTextWidthUtf(mText);
961 this(T:const(char)[]) (T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) {
962 assert(creatorCurrentParent !is null);
963 this(creatorCurrentParent, atext, horiz, vert);
966 // will be called if `isMyHotkey()` returned `true`
967 // return `true` if some action was done
968 override bool hotkeyActivated () {
969 Widget d = dest;
970 if (d is null) {
971 bool seenSelf = false;
972 d = rootWidget.forEachVisualDepth(delegate Widget (Widget w) {
973 if (!seenSelf) {
974 seenSelf = (w is this);
975 } else {
976 if (w.canAcceptFocus()) return w;
978 return null;
981 if (d is null || !d.canAcceptFocus()) return false;
982 d.focus();
983 return true;
988 // ////////////////////////////////////////////////////////////////////////// //
989 public class ProgressBarWidget : LabelWidget {
990 protected:
991 int mMin = 0;
992 int mMax = 100;
993 int mCurrent = 0;
994 int lastWidth = int.min;
995 int lastPxFull = int.min;
997 private:
998 final bool updateLast () {
999 bool res = false;
1000 if (width != lastWidth) {
1001 lastWidth = width;
1002 res = true;
1004 if (lastWidth < 1) {
1005 if (lastPxFull != 0) {
1006 res = true;
1007 lastPxFull = 0;
1009 } else {
1010 int pxFull;
1011 if (mMin == mMax) {
1012 pxFull = lastWidth;
1013 } else {
1014 pxFull = cast(int)(cast(long)lastWidth*cast(long)(mCurrent-mMin)/cast(long)(mMax-mMin));
1016 if (pxFull != lastPxFull) {
1017 res = true;
1018 lastPxFull = pxFull;
1021 if (res) postScreenRebuild();
1022 return res;
1025 public:
1026 this(T:const(char)[]) (Widget aparent, T atext, HAlign horiz=HAlign.Center, VAlign vert=VAlign.Center) {
1027 super(aparent, atext, horiz, vert);
1028 height = height+4;
1031 this(T:const(char)[]) (T atext, HAlign horiz=HAlign.Center, VAlign vert=VAlign.Center) {
1032 assert(creatorCurrentParent !is null);
1033 this(creatorCurrentParent, atext, horiz, vert);
1036 final void setMinMax (int amin, int amax) {
1037 if (amin > amax) { immutable int tmp = amin; amin = amax; amax = tmp; }
1038 mMin = amin;
1039 mMax = amax;
1040 if (mCurrent < mMin) mCurrent = mMin;
1041 if (mCurrent > mMax) mCurrent = mMax;
1042 updateLast();
1045 final @property int current () const nothrow @safe @nogc {
1046 pragma(inline, true);
1047 return mCurrent;
1050 final @property void current (int val) {
1051 pragma(inline, true);
1052 if (val < mMin) val = mMin;
1053 if (val > mMax) val = mMax;
1054 mCurrent = val;
1055 updateLast();
1058 // returns `true` if need to repaint
1059 final bool setCurrentTotal (int val, int total) {
1060 if (total < 0) total = 0;
1061 setMinMax(0, total);
1062 if (val < 0) val = 0;
1063 if (val > total) val = total;
1064 mCurrent = val;
1065 return updateLast();
1068 protected void drawStripes (GxRect rect, in bool asfull) {
1069 if (rect.empty) return;
1071 immutable int sty = rect.pos.y;
1072 immutable int sth = rect.size.h;
1074 GxRect uprc = rect;
1075 if (rect.height > 4) uprc.size.h = 2;
1076 else if (rect.height > 2) uprc.size.h = 1;
1077 else uprc.size.h = 0;
1078 if (uprc.size.h > 0) {
1079 rect.pos.y += uprc.size.h;
1080 rect.size.h -= uprc.size.h;
1083 if (!uprc.empty) gxFillRect(uprc, getColor(asfull ? "back-full-hishade" : "back-hishade"));
1084 gxFillRect(rect, getColor(asfull ? "back-full" : "back"));
1086 immutable uint clrHi = getColor(asfull ? "stripe-full-hishade" : "stripe-hishade");
1087 immutable uint clrOk = getColor(asfull ? "stripe-full" : "stripe");
1089 foreach (int y0; 0..sth) {
1090 gxHStripedLine(rect.pos.x-y0, sty+y0, rect.size.w+y0, 16, (y0 < uprc.size.h ? clrHi : clrOk));
1094 protected override void doPaint (GxRect grect) {
1095 immutable uint clrRect = getColor("rect");
1097 gxDrawRect(grect, clrRect);
1098 gxClipRect.shrinkBy(1, 1);
1099 grect.shrinkBy(1, 1);
1100 if (grect.empty) return;
1102 if (lastWidth != width) updateLast();
1103 immutable int pxFull = lastPxFull;
1105 if (pxFull > 0) {
1106 gxWithSavedClip{
1107 GxRect rc = GxRect(grect.pos, pxFull, grect.height);
1108 if (gxClipRect.intersect(rc)) drawStripes(rc, asfull:true);
1112 if (pxFull < grect.width) {
1113 gxWithSavedClip{
1114 GxRect rc = grect;
1115 rc.pos.x += pxFull;
1116 if (gxClipRect.intersect(rc)) drawStripes(grect, asfull:false);
1120 if (grect.height > 2) grect.moveLeftTopBy(0, 1);
1121 drawLabel(grect);
1126 // ////////////////////////////////////////////////////////////////////////// //
1127 public class ButtonWidget : Widget {
1128 public:
1129 enum Default {
1130 None,
1131 Accept,
1132 Cancel,
1135 protected:
1136 dynstring mTitle;
1137 int hotxpos;
1138 int hotxlen;
1140 public:
1141 Default deftype = Default.None;
1143 public:
1144 this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) {
1145 tabStop = true;
1146 super(aparent);
1147 title = atitle;
1148 hotkey = ahotkey;
1149 extractHotKey(ref mTitle, ref mHotkey, ref hotxpos, ref hotxlen);
1150 rect.size.w = gxTextWidthUtf(mTitle)+6;
1151 rect.size.h = gxTextHeightUtf+4;
1154 this(T0:const(char)[], T1:const(char)[]) (T0 atitle, T1 ahotkey=null) {
1155 assert(creatorCurrentParent !is null);
1156 this(creatorCurrentParent, atitle, ahotkey);
1159 mixin(WidgetStringPropertyMixin!("title", "mTitle"));
1160 mixin(WidgetStringPropertyMixin!("hotkey", "mHotkey"));
1162 protected override void doPaint (GxRect grect) {
1163 uint bclr = getColor("back");
1164 uint fclr = getColor("text");
1165 gxFillRect(grect.x0+1, grect.y0+1, grect.width-2, grect.height-2, bclr);
1166 gxHLine(grect.x0+1, grect.y0+0, grect.width-2, bclr);
1167 gxHLine(grect.x0+1, grect.y1+0, grect.width-2, bclr);
1168 gxVLine(grect.x0+0, grect.y0+1, grect.height-2, bclr);
1169 gxVLine(grect.x1+0, grect.y0+1, grect.height-2, bclr);
1170 gxClipRect.shrinkBy(1, 1);
1171 int tx = grect.x0+(width-gxTextWidthUtf(mTitle))/2;
1172 int ty = grect.y0+(height-gxTextHeightUtf)/2;
1173 gxDrawTextUtf(tx, ty, mTitle, fclr);
1174 if (hotxlen > 0) gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, hotxlen, getColor("hotline"));
1177 override bool onKey (KeyEvent event) {
1178 if (!event.pressed || !isFocused) return super.onKey(event);
1179 if (event == "Enter" || event == "Space") {
1180 doAction();
1181 return true;
1183 return super.onKey(event);
1186 override bool onMouse (MouseEvent event) {
1187 if (!isFocused) return super.onMouse(event);
1188 if (GxPoint(event.x, event.y).inside(rect.size)) {
1189 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
1190 //{ import std.stdio; writeln("BTN(", mTitle, "): !!!"); }
1191 doAction();
1193 return true;
1195 return super.onMouse(event);
1200 // ////////////////////////////////////////////////////////////////////////// //
1201 public class ButtonExWidget : ButtonWidget {
1202 public:
1203 this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) {
1204 super(aparent, atitle, ahotkey);
1205 rect.size.h = rect.size.h+1;
1208 this(T0:const(char)[], T1:const(char)[]) (T0 atitle, T1 ahotkey=null) {
1209 assert(creatorCurrentParent !is null);
1210 this(creatorCurrentParent, atitle, ahotkey);
1213 protected override void doPaint (GxRect grect) {
1215 uint bclr = (focused ? gxRGB!(0x93, 0xb6, 0xfc) : gxRGB!(0x73, 0x96, 0xdc));
1216 uint hclr = (focused ? gxRGB!(0xa3, 0xc6, 0xff) : gxRGB!(0x83, 0xa6, 0xec));
1217 uint fclr = (focused ? gxRGB!(0xff, 0xff, 0xff) : gxRGB!(0xff, 0xff, 0xff));
1218 uint brc = gxRGB!(0x40, 0x70, 0xcf);
1220 uint bclr = getColor("back");
1221 uint hclr = getColor("shadowline");
1222 uint fclr = getColor("text");
1223 uint brc = getColor("rect");
1225 gxFillRect(grect.x0+1, grect.y0+1, grect.width-2, grect.height-2, bclr);
1226 gxDrawRect(grect.x0, grect.y0, grect.width, grect.height, brc);
1227 gxHLine(grect.x0+1, grect.y0+1, grect.width-2, hclr);
1229 gxClipRect.shrinkBy(1, 1);
1230 int tx = grect.x0+(width-gxTextWidthUtf(mTitle))/2;
1231 int ty = grect.y0+(height-gxTextHeightUtf)/2;
1232 gxDrawTextUtf(tx, ty, mTitle, fclr);
1233 if (hotxlen > 0) gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, hotxlen, getColor("hotline"));
1238 // ////////////////////////////////////////////////////////////////////////// //
1239 public class CheckboxWidget : ButtonWidget {
1240 protected:
1241 bool mChecked;
1242 bool mDisabled;
1244 public:
1245 override string getCurrentMod () nothrow @trusted @nogc {
1246 if (mDisabled) return "disabled";
1247 return super.getCurrentMod();
1250 public:
1251 this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) {
1252 tabStop = true;
1253 super(aparent, atitle, ahotkey);
1254 width = gxTextWidthUtf(mTitle)+6+gxTextWidthUtf("[")+gxTextWidthUtf("x")+gxTextWidthUtf("]")+8;
1255 height = gxTextHeightUtf+4;
1258 this(T0:const(char)[], T1:const(char)[]) (T0 atitle, T1 ahotkey=null) {
1259 assert(creatorCurrentParent !is null);
1260 this(creatorCurrentParent, atitle, ahotkey);
1263 @property bool checked () const nothrow @safe @nogc { return mChecked; }
1264 @property void checked (in bool v) nothrow { if (mChecked != v) { mChecked = v; widgetChanged(); } }
1266 @property bool enabled () const nothrow @safe @nogc { return !mDisabled; }
1267 @property void enabled (in bool v) nothrow { if (mDisabled == v) { mDisabled = !v; tabStop = v; widgetChanged(); } }
1269 @property bool disabled () const nothrow @safe @nogc { return mDisabled; }
1270 @property void disabled (in bool v) nothrow { if (mDisabled != v) { mDisabled = v; tabStop = !v; widgetChanged(); } }
1272 override bool canAcceptFocus () { return (tabStop && !mDisabled); }
1274 protected override void doPaint (GxRect grect) {
1275 uint fclr = getColor("text");
1276 uint bclr = getColor("back");
1277 uint xclr = getColor("mark");
1279 gxFillRect(grect, bclr);
1281 gxClipRect.shrinkBy(1, 1);
1282 int tx = grect.x0+3;
1283 int ty = grect.y0+(grect.height-gxTextHeightUtf)/2;
1285 gxDrawTextUtf(tx, ty, "[", fclr);
1286 tx += gxTextWidthUtf("[");
1288 if (mChecked) gxDrawTextUtf(tx, ty, "x", xclr);
1289 tx += gxTextWidthUtf("x");
1291 gxDrawTextUtf(tx, ty, "]", fclr);
1292 tx += gxTextWidthUtf("]")+8;
1294 gxDrawTextUtf(tx, ty, mTitle, fclr);
1295 if (hotxlen > 0) gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, hotxlen, fclr);
1298 protected override void doDefaultAction () {
1299 mChecked = !mChecked;
1302 override bool onMouse (MouseEvent event) {
1303 if (!isFocused) return super.onMouse(event);
1304 if (GxPoint(event.x, event.y).inside(rect.size)) {
1305 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1306 doAction();
1308 return true;
1310 return super.onMouse(event);
1315 // ////////////////////////////////////////////////////////////////////////// //
1316 public final class SimpleListBoxWidget : Widget {
1317 private:
1318 dynstring[] mItems;
1319 Object[] mItemData;
1320 int mTopIdx;
1321 int mCurIdx;
1323 public:
1324 this (Widget aparent) {
1325 super(aparent);
1328 this () {
1329 assert(creatorCurrentParent !is null);
1330 this(creatorCurrentParent);
1333 @property int curidx () const nothrow @safe @nogc { return (mCurIdx >= 0 && mCurIdx < mItems.length ? mCurIdx : 0); }
1335 @property void curidx (int idx) nothrow @safe @nogc {
1336 if (mItems.length == 0) return;
1337 if (idx < 0) idx = 0;
1338 if (idx >= mItems.length) idx = cast(int)(mItems.length-1);
1339 mCurIdx = idx;
1342 @property int length () const nothrow @safe @nogc { return cast(int)mItems.length; }
1343 @property dynstring opIndex (usize idx) const nothrow @safe @nogc { return (idx < mItems.length ? mItems[idx] : dynstring()); }
1344 @property Object itemData (usize idx) nothrow @safe @nogc { return (idx < mItems.length ? mItemData[idx] : null); }
1346 void appendItem(T:const(char)[]) (T s, Object o=null) {
1347 //conwriteln("new item: ", s);
1348 dynstring it = s;
1349 mItems ~= it;
1350 mItemData ~= o;
1353 @property int visibleItemsCount () const nothrow @trusted {
1354 pragma(inline, true);
1355 return (height < gxTextHeightUtf*2 ? 1 : height/gxTextHeightUtf);
1358 void makeCursorVisible () {
1359 if (mItems.length == 0) return;
1360 if (mCurIdx < 0) mCurIdx = 0;
1361 if (mCurIdx >= mItems.length) mCurIdx = cast(int)(mItems.length-1);
1362 if (mCurIdx < mTopIdx) { mTopIdx = mCurIdx; return; }
1363 int icnt = visibleItemsCount-1;
1364 if (mTopIdx+icnt < mCurIdx) mTopIdx = mCurIdx-icnt;
1367 protected override void doPaint (GxRect grect) {
1368 makeCursorVisible();
1369 uint bclr = getColor("back");
1370 uint tclr = getColor("text");
1371 uint cbclr = getColor("cursor-back");
1372 uint ctclr = getColor("cursor-text");
1373 gxFillRect(grect, bclr);
1374 int y = 0;
1375 int idx = mTopIdx;
1376 while (idx < mItems.length && y < grect.height) {
1377 if (idx >= 0) {
1378 uint clr = tclr;
1379 if (idx == mCurIdx) {
1380 gxFillRect(grect.x0, grect.y0+y, grect.width, gxTextHeightUtf, cbclr);
1381 clr = ctclr;
1383 gxWithSavedClip {
1384 gxClipRect.intersect(GxRect(grect.pos, GxPoint(grect.x1-1, grect.y1)));
1385 gxDrawTextUtf(grect.x0+1, grect.y0+y, mItems[idx], clr);
1388 ++idx;
1389 y += gxTextHeightUtf;
1393 override bool onKey (KeyEvent event) {
1394 if (!event.pressed || !isFocused) return super.onKey(event);
1395 if (event == "Up") { curidx = curidx-1; return true; }
1396 if (event == "Down") { curidx = curidx+1; return true; }
1397 if (event == "Home") { curidx = 0; return true; }
1398 if (event == "End") { curidx = cast(int)(mItems.length-1); return true; }
1399 if (event == "PageUp") {
1400 makeCursorVisible();
1401 if (curidx > mTopIdx) {
1402 curidx = mTopIdx;
1403 } else {
1404 curidx = curidx-(visibleItemsCount-1);
1406 return true;
1408 if (event == "PageDown") {
1409 makeCursorVisible();
1410 int icnt = visibleItemsCount-1;
1411 if (icnt) {
1412 if (mTopIdx+icnt < curidx) {
1413 curidx = mTopIdx+icnt;
1414 } else {
1415 curidx = curidx+icnt;
1418 return true;
1420 return super.onKey(event);
1423 override bool onMouse (MouseEvent event) {
1424 if (!isFocused) return super.onMouse(event);
1425 if (GxPoint(event.x, event.y).inside(rect.size)) {
1426 int mx = event.x, my = event.y;
1427 makeCursorVisible();
1428 int idx = mTopIdx+my/gxTextHeightUtf;
1429 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1430 if (curidx == idx) doAction(); else curidx = idx;
1431 } else if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelUp) {
1432 curidx = curidx-1;
1433 } else if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelDown) {
1434 curidx = curidx+1;
1436 return true;
1438 return super.onMouse(event);