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*/;
24 import arsd
.simpledisplay
;
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();
55 // ////////////////////////////////////////////////////////////////////////// //
56 public abstract class Widget
: EgraStyledClass
{
58 static template isGoodEnterBoxDelegate(DG
) {
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
); }));
77 GxRect rect
; // relative to parent
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
85 // this is for flexbox layouter
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)
124 if (prefSize
.w
== int.min
) prefSize
= rect
.size
; else rect
.size
= prefSize
;
130 final switch (boxHAlign
) with (BoxHAlign
) {
131 case Expand
: rect
.size
.w
= finalSize
.w
; 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;
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;
145 Widget mActive
; // do not change directly!
149 bool isMyHotkey (KeyEvent event
) {
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;
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
;
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;
191 dynstring
getHotkey () { return mHotkey
; }
192 void setHotkey (const(char)[] v
) { mHotkey
= v
; }
194 bool delegate (Widget self
, KeyEvent event
) onCheckHotkey
;
197 void delegate (Widget self
) onAction
;
199 protected void doDefaultAction () {}
202 if (onAction
!is null) onAction(this); else doDefaultAction();
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..$];
222 hotxpos
= gxTextWidthUtf(text
[0..umpos
]);
223 hotxlen
= gxTextWidthUtf(hotch
);
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;
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");
237 Widget lc
= firstChild
;
241 while (lc
.nextSibling
) lc
= lc
.nextSibling
;
244 w
.invalidatePathCache();
247 //if (mActive is null && w.canAcceptFocus(w)) mActive = w;
250 Widget
rootWidget () {
252 while (res
.parent
) res
= res
.parent
;
256 static template isFEDGoodDelegate(DG
) {
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
) {
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
) {
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
) {
288 Widget res
= w
.forEachVisualDepth(dg
);
289 if (res
!is null) return res
;
292 static if (isFEDResDelegate
!DG
) {
300 static template isGoodIteratorDelegate(DG
) {
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
{
311 this (Widget ww
, in bool onlyvis
) nothrow @safe @nogc {
312 pragma(inline
, true);
314 onlyVisual
= onlyvis
;
317 int opApply(DG
) (scope DG dg
) if (isGoodIteratorDelegate
!DG
) {
319 if (w
is null || dg
is null) return 0;
321 w
.forEachVisualDepth((Widget w
) {
323 return (res ? w
: null);
326 w
.forEachDepth((Widget w
) {
328 return (res ? w
: null);
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
); }
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);
352 Widget
enter(DG
) (scope DG dg
) if (isGoodEnterBoxDelegate
!DG
) {
354 Widget oldCCP
= creatorCurrentParent
;
355 creatorCurrentParent
= this;
356 scope(exit
) creatorCurrentParent
= oldCCP
;
357 static if (isEnterBoxDelegateWithArgs
!DG
) {
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);
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();
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();
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
;
409 while (res
.mActive
!is null) res
= res
.mActive
;
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;
424 while (w
.parent
!is null) {
425 w
.parent
.mActive
= w
;
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;
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
) {}
512 if (nonVisual || rect
.empty
) return; // nothing to paint here
513 if (gxClipRect
.empty
) return; // totally clipped away
515 immutable GxRect grect
= globalRect
;
516 if (!gxClipRect
.intersect(grect
)) return;
517 // before-children painter
518 gxWithSavedClip
{ doPaint(grect
); };
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
537 // called after setting the focus
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; }
558 // all coords are global
559 void drawTextCursor (int x
, int y
, int hgt
=-666) {
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");
572 gxVLine(x
, y
, hgt
, clr
);
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
);
581 immutable uint clrDot
= getColor("text-cursor-dot");
582 if (hgt
> 2 && !gxIsTransparent(clrDot
)) {
583 ctm
= getInt("text-cursor-dot-time", 0);
586 int doty
= msecs
/ctm
%(hgt
*2-1);
587 if (doty
>= hgt
) doty
= hgt
*2-doty
-1;
589 gxPutPixel(x
, y
+doty
+1, clrDot
);
591 gxHLine(x
, y
+doty
+1, 2, clrDot
);
595 if (ctm
>= 20 || btm
>= 20) {
596 if (!ctm || ctm
> (btm
>>1)) ctm
= (btm
>>1);
604 // ////////////////////////////////////////////////////////////////////////// //
605 // this is root widget that is used for subwindow client area
606 public class RootWidget
: Widget
{
608 uint mButtons
; // used for grab
610 override EgraStyledClass
getParent () nothrow @trusted @nogc { return owner
; }
614 this (SubWindow aowner
) {
615 childDir
= GxDir
.Vert
;
618 if (aowner
!is null) aowner
.setRoot(this);
621 override bool isOwnerFocused () nothrow @safe @nogc {
622 if (owner
is null) return false;
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 () {
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
;
643 if (mButtons
&& !isOwnerFocused
) releaseGrab();
648 return (mButtons
!= 0);
657 // dispatch event to this widget
658 // it implements sink/bubble model
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;
686 Widget fc
= focusedWidget
;
687 foreach (Widget w
; allVisualDepth
) {
689 if (w
.canAcceptFocus()) pf
= w
;
694 foreach (Widget w
; allVisualDepth
) {
695 if (w
.canAcceptFocus()) pf
= w
;
699 if (pf
!is null) pf
.focus();
703 Widget fc
= focusedWidget
;
704 bool seenFC
= (fc
is null);
706 foreach (Widget w
; allVisualDepth
) {
710 if (w
.canAcceptFocus()) { nf
= w
; break; }
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
) {
726 if (event
== "Tab" || event
== "C-Tab") { doTab(); return true; }
727 if (event
== "S-Tab" || event
== "C-S-Tab") { doShiftTab(); return true; }
730 immutable bool isDefAccept
= (event
== "Enter");
731 immutable bool isDefCancel
= (event
== "Escape");
735 foreach (Widget w
; allVisualDepth
) {
736 if (w
.isMyHotkey(event
) && w
.hotkeyActivated()) {
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
) {
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
) {
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
;
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
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;
821 immutable GxRect wrect
= curr
.globalRect
;
822 MouseEvent ev
= event
;
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
) {
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);
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
) {
871 this (in int aflex
) {
872 assert(creatorCurrentParent
!is null);
873 this(creatorCurrentParent
, aflex
);
878 // ////////////////////////////////////////////////////////////////////////// //
879 public class BoxWidget
: Widget
{
880 this (Widget aparent
) {
886 assert(creatorCurrentParent
!is null);
887 this(creatorCurrentParent
);
891 public class HBoxWidget
: BoxWidget
{
892 this (Widget aparent
) {
894 childDir
= GxDir
.Horiz
;
898 assert(creatorCurrentParent
!is null);
899 this(creatorCurrentParent
);
903 public class VBoxWidget
: BoxWidget
{
904 this (Widget aparent
) {
906 childDir
= GxDir
.Vert
;
910 assert(creatorCurrentParent
!is null);
911 this(creatorCurrentParent
);
916 // ////////////////////////////////////////////////////////////////////////// //
917 public class LabelWidget
: Widget
{
937 HAlign halign
= HAlign
.Left
;
938 VAlign valign
= VAlign
.Center
;
943 this(T
:const(char)[]) (Widget aparent
, T atext
, HAlign horiz
=HAlign
.Left
, VAlign vert
=VAlign
.Center
) {
946 rect
.size
.w
= gxTextWidthUtf(mText
);
947 rect
.size
.h
= gxTextHeightUtf
;
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;
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;
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"));
988 // ////////////////////////////////////////////////////////////////////////// //
989 public class HotLabelWidget
: LabelWidget
{
991 Widget dest
; // if `null`, activate next focusable sibling
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 () {
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
) {
1014 seenSelf
= (w
is this);
1016 if (w
.canAcceptFocus()) { d
= w
; break; }
1020 if (d
is null ||
!d
.canAcceptFocus()) return false;
1027 // ////////////////////////////////////////////////////////////////////////// //
1028 public class ProgressBarWidget
: LabelWidget
{
1033 int lastWidth
= int.min
;
1034 int lastPxFull
= int.min
;
1037 final bool updateLast () {
1039 if (width
!= lastWidth
) {
1043 if (lastWidth
< 1) {
1044 if (lastPxFull
!= 0) {
1053 pxFull
= cast(int)(cast(long)lastWidth
*cast(long)(mCurrent
-mMin
)/cast(long)(mMax
-mMin
));
1055 if (pxFull
!= lastPxFull
) {
1057 lastPxFull
= pxFull
;
1060 if (res
) widgetChanged();
1065 this(T
:const(char)[]) (Widget aparent
, T atext
, HAlign horiz
=HAlign
.Center
, VAlign vert
=VAlign
.Center
) {
1066 super(aparent
, atext
, horiz
, vert
);
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
; }
1079 if (mCurrent
< mMin
) mCurrent
= mMin
;
1080 if (mCurrent
> mMax
) mCurrent
= mMax
;
1084 final @property int current () const nothrow @safe @nogc {
1085 pragma(inline
, true);
1089 final @property void current (int val
) {
1090 pragma(inline
, true);
1091 if (val
< mMin
) val
= mMin
;
1092 if (val
> mMax
) val
= mMax
;
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
;
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
;
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
;
1146 GxRect rc
= GxRect(grect
.pos
, pxFull
, grect
.height
);
1147 if (gxClipRect
.intersect(rc
)) drawStripes(rc
, asfull
:true);
1151 if (pxFull
< grect
.width
) {
1155 if (gxClipRect
.intersect(rc
)) drawStripes(grect
, asfull
:false);
1159 if (grect
.height
> 2) grect
.moveLeftTopBy(0, 1);
1165 // ////////////////////////////////////////////////////////////////////////// //
1166 public abstract class BaseButtonWidget
: Widget
{
1180 Default deftype
= Default
.None
;
1183 this(T0
:const(char)[], T1
:const(char)[]) (Widget aparent
, T0 atitle
, T1 ahotkey
=null) {
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") {
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, "): !!!"); }
1219 return super.onMouse(event
);
1224 // ////////////////////////////////////////////////////////////////////////// //
1225 public class ButtonWidget
: BaseButtonWidget
{
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
{
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
{
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();
1303 this(T0
:const(char)[], T1
:const(char)[]) (Widget aparent
, T0 atitle
, T1 ahotkey
=null) {
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
{
1365 this (Widget aparent
) {
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
) {
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);
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
);
1423 while (idx
< mItems
.length
&& y
< grect
.height
) {
1426 if (idx
== mCurIdx
) {
1427 gxFillRect(grect
.x0
, grect
.y0
+y
, grect
.width
, gxTextHeightUtf
, cbclr
);
1431 gxClipRect
.intersect(GxRect(grect
.pos
, GxPoint(grect
.x1
-1, grect
.y1
)));
1432 gxDrawTextUtf(grect
.x0
+1, grect
.y0
+y
, mItems
[idx
], clr
);
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
) {
1451 curidx
= curidx
-(visibleItemsCount
-1);
1455 if (event
== "PageDown") {
1456 makeCursorVisible();
1457 int icnt
= visibleItemsCount
-1;
1459 if (mTopIdx
+icnt
< curidx
) {
1460 curidx
= mTopIdx
+icnt
;
1462 curidx
= curidx
+icnt
;
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
) {
1480 } else if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.wheelDown
) {
1485 return super.onMouse(event
);