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 //if (mActive is null && w.canAcceptFocus(w)) mActive = w;
247 Widget
rootWidget () {
249 while (res
.parent
) res
= res
.parent
;
253 static template isFEDGoodDelegate(DG
) {
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
) {
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
) {
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
) {
285 Widget res
= w
.forEachVisualDepth(dg
);
286 if (res
!is null) return res
;
289 static if (isFEDResDelegate
!DG
) {
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);
310 Widget
enter(DG
) (scope DG dg
) if (isGoodEnterBoxDelegate
!DG
) {
312 Widget oldCCP
= creatorCurrentParent
;
313 creatorCurrentParent
= this;
314 scope(exit
) creatorCurrentParent
= oldCCP
;
315 static if (isEnterBoxDelegateWithArgs
!DG
) {
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);
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();
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();
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
;
367 while (res
.mActive
!is null) res
= res
.mActive
;
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;
382 while (w
.parent
!is null) {
383 w
.parent
.mActive
= w
;
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;
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
) {}
470 if (nonVisual || rect
.empty
) return; // nothing to paint here
471 if (gxClipRect
.empty
) return; // totally clipped away
473 immutable GxRect grect
= globalRect
;
474 if (!gxClipRect
.intersect(grect
)) return;
475 // before-children painter
476 gxWithSavedClip
{ doPaint(grect
); };
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
495 // called after setting the focus
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; }
516 // all coords are global
517 void drawTextCursor (int x
, int y
, int hgt
=-666) {
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);
528 bool ctt
= (btm
>= 10 ?
(msecs
%btm
< btm
/2) : false);
529 immutable uint clr
= getColor(ctt ?
"text-cursor-0" : "text-cursor-1");
531 gxVLine(x
, y
, hgt
, clr
);
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
);
540 ctm
= getInt("text-cursor-dot-time", 0);
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");
547 gxPutPixel(x
, y
+doty
+1, clrDot
);
549 gxHLine(x
, y
+doty
+1, 2, clrDot
);
553 if (ctm
>= 10 || btm
>= 10) {
554 if (!ctm || ctm
> btm
) ctm
= btm
;
562 // ////////////////////////////////////////////////////////////////////////// //
563 // this is root widget that is used for subwindow client area
564 public class RootWidget
: Widget
{
566 uint mButtons
; // used for grab
568 override EgraStyledClass
getParent () nothrow @trusted @nogc { return owner
; }
572 this (SubWindow aowner
) {
573 childDir
= GxDir
.Vert
;
576 if (aowner
!is null) aowner
.setRoot(this);
579 override bool isOwnerFocused () nothrow @safe @nogc {
580 if (owner
is null) return false;
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 () {
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
;
601 if (mButtons
&& !isOwnerFocused
) releaseGrab();
606 return (mButtons
!= 0);
615 // dispatch event to this widget
616 // it implements sink/bubble model
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;
644 Widget fc
= focusedWidget
;
645 forEachVisualDepth(delegate Widget (Widget w
) {
646 if (w
is fc
) return w
;
647 if (w
.canAcceptFocus()) {
649 if (fc
is null) return w
;
656 forEachVisualDepth((Widget w
) {
657 if (w
.canAcceptFocus()) pf
= w
;
661 if (pf
!is null) pf
.focus();
665 Widget fc
= focusedWidget
;
666 bool seenFC
= (fc
is null);
667 Widget nf
= forEachVisualDepth(delegate Widget (Widget w
) {
671 if (w
.canAcceptFocus()) return w
;
678 nf
= forEachVisualDepth(delegate Widget (Widget w
) {
679 if (w
.canAcceptFocus()) return w
;
684 if (nf
!is null) nf
.focus();
687 override bool onKeyBubble (Widget dest
, KeyEvent event
) {
689 if (event
== "Tab" || event
== "C-Tab") { doTab(); return true; }
690 if (event
== "S-Tab" || event
== "C-S-Tab") { doShiftTab(); return true; }
693 immutable bool isDefAccept
= (event
== "Enter");
694 immutable bool isDefCancel
= (event
== "Escape");
697 Widget hk
= forEachVisualDepth(delegate Widget (Widget w
) {
698 if (w
.isMyHotkey(event
) && w
.hotkeyActivated()) {
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
;
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
) {
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
) {
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
;
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
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;
783 immutable GxRect wrect
= curr
.globalRect
;
784 MouseEvent ev
= event
;
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
) {
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);
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
) {
833 this (in int aflex
) {
834 assert(creatorCurrentParent
!is null);
835 this(creatorCurrentParent
, aflex
);
840 // ////////////////////////////////////////////////////////////////////////// //
841 public class BoxWidget
: Widget
{
842 this (Widget aparent
) {
848 assert(creatorCurrentParent
!is null);
849 this(creatorCurrentParent
);
853 public class HBoxWidget
: BoxWidget
{
854 this (Widget aparent
) {
856 childDir
= GxDir
.Horiz
;
860 assert(creatorCurrentParent
!is null);
861 this(creatorCurrentParent
);
865 public class VBoxWidget
: BoxWidget
{
866 this (Widget aparent
) {
868 childDir
= GxDir
.Vert
;
872 assert(creatorCurrentParent
!is null);
873 this(creatorCurrentParent
);
878 // ////////////////////////////////////////////////////////////////////////// //
879 public class LabelWidget
: Widget
{
899 HAlign halign
= HAlign
.Left
;
900 VAlign valign
= VAlign
.Center
;
905 this(T
:const(char)[]) (Widget aparent
, T atext
, HAlign horiz
=HAlign
.Left
, VAlign vert
=VAlign
.Center
) {
908 rect
.size
.w
= gxTextWidthUtf(mText
);
909 rect
.size
.h
= gxTextHeightUtf
;
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;
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;
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"));
950 // ////////////////////////////////////////////////////////////////////////// //
951 public class HotLabelWidget
: LabelWidget
{
953 Widget dest
; // if `null`, activate next focusable sibling
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 () {
971 bool seenSelf
= false;
972 d
= rootWidget
.forEachVisualDepth(delegate Widget (Widget w
) {
974 seenSelf
= (w
is this);
976 if (w
.canAcceptFocus()) return w
;
981 if (d
is null ||
!d
.canAcceptFocus()) return false;
988 // ////////////////////////////////////////////////////////////////////////// //
989 public class ProgressBarWidget
: LabelWidget
{
994 int lastWidth
= int.min
;
995 int lastPxFull
= int.min
;
998 final bool updateLast () {
1000 if (width
!= lastWidth
) {
1004 if (lastWidth
< 1) {
1005 if (lastPxFull
!= 0) {
1014 pxFull
= cast(int)(cast(long)lastWidth
*cast(long)(mCurrent
-mMin
)/cast(long)(mMax
-mMin
));
1016 if (pxFull
!= lastPxFull
) {
1018 lastPxFull
= pxFull
;
1021 if (res
) postScreenRebuild();
1026 this(T
:const(char)[]) (Widget aparent
, T atext
, HAlign horiz
=HAlign
.Center
, VAlign vert
=VAlign
.Center
) {
1027 super(aparent
, atext
, horiz
, vert
);
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
; }
1040 if (mCurrent
< mMin
) mCurrent
= mMin
;
1041 if (mCurrent
> mMax
) mCurrent
= mMax
;
1045 final @property int current () const nothrow @safe @nogc {
1046 pragma(inline
, true);
1050 final @property void current (int val
) {
1051 pragma(inline
, true);
1052 if (val
< mMin
) val
= mMin
;
1053 if (val
> mMax
) val
= mMax
;
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
;
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
;
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
;
1107 GxRect rc
= GxRect(grect
.pos
, pxFull
, grect
.height
);
1108 if (gxClipRect
.intersect(rc
)) drawStripes(rc
, asfull
:true);
1112 if (pxFull
< grect
.width
) {
1116 if (gxClipRect
.intersect(rc
)) drawStripes(grect
, asfull
:false);
1120 if (grect
.height
> 2) grect
.moveLeftTopBy(0, 1);
1126 // ////////////////////////////////////////////////////////////////////////// //
1127 public class ButtonWidget
: Widget
{
1141 Default deftype
= Default
.None
;
1144 this(T0
:const(char)[], T1
:const(char)[]) (Widget aparent
, T0 atitle
, T1 ahotkey
=null) {
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") {
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, "): !!!"); }
1195 return super.onMouse(event
);
1200 // ////////////////////////////////////////////////////////////////////////// //
1201 public class ButtonExWidget
: ButtonWidget
{
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
{
1245 override string
getCurrentMod () nothrow @trusted @nogc {
1246 if (mDisabled
) return "disabled";
1247 return super.getCurrentMod();
1251 this(T0
:const(char)[], T1
:const(char)[]) (Widget aparent
, T0 atitle
, T1 ahotkey
=null) {
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
) {
1310 return super.onMouse(event
);
1315 // ////////////////////////////////////////////////////////////////////////// //
1316 public final class SimpleListBoxWidget
: Widget
{
1324 this (Widget aparent
) {
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);
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);
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
);
1376 while (idx
< mItems
.length
&& y
< grect
.height
) {
1379 if (idx
== mCurIdx
) {
1380 gxFillRect(grect
.x0
, grect
.y0
+y
, grect
.width
, gxTextHeightUtf
, cbclr
);
1384 gxClipRect
.intersect(GxRect(grect
.pos
, GxPoint(grect
.x1
-1, grect
.y1
)));
1385 gxDrawTextUtf(grect
.x0
+1, grect
.y0
+y
, mItems
[idx
], clr
);
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
) {
1404 curidx
= curidx
-(visibleItemsCount
-1);
1408 if (event
== "PageDown") {
1409 makeCursorVisible();
1410 int icnt
= visibleItemsCount
-1;
1412 if (mTopIdx
+icnt
< curidx
) {
1413 curidx
= mTopIdx
+icnt
;
1415 curidx
= curidx
+icnt
;
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
) {
1433 } else if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.wheelDown
) {
1438 return super.onMouse(event
);