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
.base
/*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;
190 final T
querySelector(T
:EgraStyledClass
=Widget
) (const(char)[] sel
) { pragma(inline
, true); return querySelectorInternal
!T(sel
); }
191 final auto querySelectorAll(T
:EgraStyledClass
=Widget
) (const(char)[] sel
) nothrow @safe @nogc { pragma(inline
, true); return Iter
!T(this, sel
); }
194 dynstring
getHotkey () { return mHotkey
; }
195 void setHotkey (const(char)[] v
) { mHotkey
= v
; }
197 bool delegate (Widget self
, KeyEvent event
) onCheckHotkey
;
200 void delegate (Widget self
) onAction
;
202 protected void doDefaultAction () {}
205 if (onAction
!is null) onAction(this); else doDefaultAction();
210 static bool extractHotKey (ref dynstring text
, ref dynstring hotch
, ref int hotxpos
, ref int hotxlen
) nothrow @safe {
211 if (text
.length
< 2) return false;
212 auto umpos
= text
.indexOf('&');
213 if (umpos
< 0 || umpos
>= text
.length
-1) return false;
214 char ch
= text
[umpos
+1];
215 if (ch
> 32 && ch
< 128) {
216 hotch
= text
[umpos
+1..umpos
+2];
217 text
= text
[0..umpos
]~text
[umpos
+1..$];
218 } else if (ch
>= 128) {
219 hotch
= text
[umpos
+1..$];
220 while (hotch
.utflen
> 1) hotch
= hotch
.utfchop
;
221 text
= text
[0..umpos
]~text
[umpos
+1..$];
225 hotxpos
= gxTextWidthUtf(text
[0..umpos
]);
226 hotxlen
= gxTextWidthUtf(hotch
);
232 void addChild (Widget w
) {
233 //{ import std.stdio; writefln("addChild(0x%08x); w=0x%08x", cast(uint)cast(void*)this, cast(uint)cast(void*)w); }
234 if (w
is null) return;
236 assert(w
.parent
is null);
237 if (cast(RootWidget
)w
) throw new Exception("root widget cannot be child of anything");
238 if (nonVisual
) throw new Exception("non-visual widget cannot have children");
240 Widget lc
= firstChild
;
244 while (lc
.nextSibling
) lc
= lc
.nextSibling
;
247 w
.invalidatePathCache();
250 //if (mActive is null && w.canAcceptFocus(w)) mActive = w;
253 Widget
rootWidget () nothrow {
255 while (res
.parent
) res
= res
.parent
;
259 static template isFEDGoodDelegate(DG
) {
261 enum isFEDGoodDelegate
=
262 (is(ReturnType
!DG
== void) ||
is(ReturnType
!DG
== Widget
)) &&
263 (is(typeof((inout int=0) { DG dg
= void; Widget w
; Widget res
= dg(w
); })) ||
264 is(typeof((inout int=0) { DG dg
= void; Widget w
; dg(w
); })));
267 static template isFEDResDelegate(DG
) {
269 enum isFEDResDelegate
= is(ReturnType
!DG
== Widget
);
272 // Widget delegate (Widget w)
273 final Widget
forEachDepth(DG
) (scope DG dg
) if (isFEDGoodDelegate
!DG
) {
274 for (Widget w
= firstChild
; w
!is null; w
= w
.nextSibling
) {
275 Widget res
= w
.forEachDepth(dg
);
276 if (res
!is null) return res
;
278 static if (isFEDResDelegate
!DG
) {
286 // Widget delegate (Widget w)
287 final Widget
forEachVisualDepth(DG
) (scope DG dg
) if (isFEDGoodDelegate
!DG
) {
288 if (nonVisual
) return null;
289 for (Widget w
= firstChild
; w
!is null; w
= w
.nextSibling
) {
291 Widget res
= w
.forEachVisualDepth(dg
);
292 if (res
!is null) return res
;
295 static if (isFEDResDelegate
!DG
) {
303 static template isGoodIteratorDelegate(DG
) {
305 enum isGoodIteratorDelegate
=
306 (is(ReturnType
!DG
== int)) &&
307 is(typeof((inout int=0) { DG dg
= void; Widget w
; int res
= dg(w
); }));
310 static struct IterChild
{
314 this (Widget ww
, in bool onlyvis
) nothrow @safe @nogc {
315 pragma(inline
, true);
317 onlyVisual
= onlyvis
;
320 int opApply(DG
) (scope DG dg
) if (isGoodIteratorDelegate
!DG
) {
322 if (w
is null || dg
is null) return 0;
324 w
.forEachVisualDepth((Widget w
) {
326 return (res ? w
: null);
329 w
.forEachDepth((Widget w
) {
331 return (res ? w
: null);
338 final IterChild
allDepth () nothrow @safe @nogc { pragma(inline
, true); return IterChild(this, false); }
339 final IterChild
allVisualDepth () nothrow @safe @nogc { pragma(inline
, true); return IterChild(this, true); }
340 final IterChild
allDepth (in bool onlyvis
) nothrow @safe @nogc { pragma(inline
, true); return IterChild(this, onlyvis
); }
344 assert(creatorCurrentParent
!is null);
345 assert(cast(RootWidget
)this is null);
346 creatorCurrentParent
.addChild(this);
349 this (Widget aparent
) {
350 if (aparent
!is null) aparent
.addChild(this);
355 Widget
enter(DG
) (scope DG dg
) if (isGoodEnterBoxDelegate
!DG
) {
357 Widget oldCCP
= creatorCurrentParent
;
358 creatorCurrentParent
= this;
359 scope(exit
) creatorCurrentParent
= oldCCP
;
360 static if (isEnterBoxDelegateWithArgs
!DG
) {
369 static Widget
getCurrentBox () nothrow @safe @nogc {
370 pragma(inline
, true);
371 return creatorCurrentParent
;
375 // returns `true` if passed `this`
376 final bool isMyChild (in Widget w
) pure const nothrow @safe @nogc {
377 pragma(inline
, true);
384 // should be overriden in root widget
385 // should be called only for widgets without a parent
386 bool isOwnerFocused () nothrow @safe @nogc {
387 if (parent
) return parent
.isOwnerFocused();
391 // should be overriden in root widget
392 // should be called only for widgets without a parent
393 bool isOwnerInWindowList () nothrow @safe @nogc {
394 if (parent
) return parent
.isOwnerInWindowList();
398 // should be overriden in the root widget
399 void releaseGrab () {
402 // should be overriden in the root widget
403 GxPoint
getGlobalOffset () nothrow @safe {
404 return GxPoint(0, 0);
407 // returns focused widget; it doesn't matter if root widget's owner is focused
408 // never returns `null`
409 @property Widget
focusedWidget () nothrow @safe @nogc {
410 if (parent
) return parent
.focusedWidget
;
412 while (res
.mActive
!is null) res
= res
.mActive
;
416 // it doesn't matter if root widget's owner is focused
417 //FIXME: ask all parents too?
418 final @property bool focus () {
419 Widget oldFocus
= focusedWidget
;
420 if (oldFocus
is this) return true;
421 if (!canAcceptFocus()) return false;
422 if (!oldFocus
.canRemoveFocus()) return false;
427 while (w
.parent
!is null) {
428 w
.parent
.mActive
= w
;
435 @property void setTabStop (in bool v
) nothrow {
436 if (tabStop
== v
) return;
438 if (!v
&& isFocused
) {
439 RootWidget rw
= cast(RootWidget
)rootWidget
;
441 try {rw
.doTab(); } catch (Exception e
) {} // sorry
446 // this returns `false` if root widget's parent is not focused
447 final @property bool isFocused () nothrow @safe @nogc {
448 pragma(inline
, true);
449 return (isOwnerFocused() && this is focusedWidget
);
452 // this ignores root widget's parent focus
453 final @property bool isFocusedForStyle () nothrow @safe @nogc {
454 pragma(inline
, true);
455 return (this is focusedWidget
&& (isOwnerFocused ||
!isOwnerInWindowList
));
458 // returns point translated to the topmost parent coords
459 // with proper root widget, returns global screen coordinates (useful for drawing)
460 final @property GxPoint
point2Global (GxPoint pt
) nothrow @safe {
461 if (parent
is null) return pt
+getGlobalOffset
;
462 return parent
.point2Global(pt
+rect
.pos
);
465 // returns rectangle in topmost parent coords
466 // with proper root widget, returns global screen coordinates (useful for drawing)
467 final @property GxRect
globalRect () nothrow @safe {
468 GxPoint pt
= point2Global(GxPoint(0, 0));
469 return GxRect(pt
, rect
.size
);
472 // this need to be called if you did automatic layouting, and then changed size or position
473 // it's not done automatically because it is rarely required
474 final void rectChanged () nothrow @safe @nogc {
475 pragma(inline
, true);
476 prefSize
= GxSize(int.min
, int.min
);
479 final @property ref inout(GxSize
) size () inout pure nothrow @safe @nogc { pragma(inline
, true); return rect
.size
; }
480 final @property void size (in GxSize sz
) nothrow { pragma(inline
, true); if (rect
.size
!= sz
) { rect
.size
= sz
; widgetChanged(); } }
482 final @property ref inout(GxPoint
) pos () inout pure nothrow @safe @nogc { pragma(inline
, true); return rect
.pos
; }
483 final @property void pos (in GxPoint p
) nothrow { pragma(inline
, true); if (rect
.pos
!= p
) { rect
.pos
= p
; widgetChanged(); } }
485 final @property int width () const pure nothrow @safe @nogc { pragma(inline
, true); return rect
.size
.w
; }
486 final @property int height () const pure nothrow @safe @nogc { pragma(inline
, true); return rect
.size
.h
; }
488 final @property void width (in int v
) nothrow { pragma(inline
, true); if (rect
.size
.w
!= v
) { rect
.size
.w
= v
; widgetChanged(); } }
489 final @property void height (in int v
) nothrow { pragma(inline
, true); if (rect
.size
.h
!= v
) { rect
.size
.h
= v
; widgetChanged(); } }
491 final @property int posx () const pure nothrow @safe @nogc { pragma(inline
, true); return rect
.pos
.x
; }
492 final @property int posy () const pure nothrow @safe @nogc { pragma(inline
, true); return rect
.pos
.y
; }
494 final @property void posx (in int v
) nothrow { pragma(inline
, true); if (rect
.pos
.x
!= v
) { rect
.pos
.x
= v
; widgetChanged(); } }
495 final @property void posy (in int v
) nothrow { pragma(inline
, true); if (rect
.pos
.y
!= v
) { rect
.pos
.y
= v
; widgetChanged(); } }
497 // this never returns `null`, even for out-of-bound coords
498 // for out-of-bounds case simply return `this`
499 final Widget
childAt (GxPoint pt
) pure nothrow @safe @nogc {
500 if (!pt
.inside(rect
.size
)) return this;
502 for (Widget w
= firstChild
; w
!is null; w
= w
.nextSibling
) {
503 if (!w
.nonVisual
&& pt
.inside(w
.rect
)) bestChild
= w
;
505 if (bestChild
is null) return this;
506 pt
-= bestChild
.rect
.pos
;
507 return bestChild
.childAt(pt
);
510 final Widget
childAt (in int x
, in int y
) pure nothrow @safe @nogc {
511 pragma(inline
, true);
512 return childAt(GxPoint(x
, y
));
515 // this is called with set clip
516 // passed rectangle is global rect
517 // clip rect is set to the widget area
518 // `doPaint()` is called before painting children
519 // `doPaintPost()` is called after painting children
520 // it is safe to change clip rect there, it will be restored
521 // `grect` is in global screen coordinates
522 protected void doPaint (GxRect grect
) {}
523 protected void doPaintPost (GxRect grect
) {}
526 if (nonVisual || rect
.empty
) return; // nothing to paint here
527 if (gxClipRect
.empty
) return; // totally clipped away
529 immutable GxRect grect
= globalRect
;
530 if (!gxClipRect
.intersect(grect
)) return;
531 // before-children painter
532 gxWithSavedClip
{ doPaint(grect
); };
534 for (Widget w
= firstChild
; w
!is null; w
= w
.nextSibling
) {
535 if (!w
.nonVisual
) gxWithSavedClip
{ w
.onPaint(); };
537 // after-children painter
538 gxWithSavedClip
{ doPaintPost(grect
); };
542 // return `false` to disable focus change
543 bool canRemoveFocus () { return true; }
545 // return `false` to disable focus change
546 bool canAcceptFocus () { return (!nonVisual
&& tabStop
); }
548 // called before removing the focus
551 // called after setting the focus
554 // return `true` if the event was eaten
555 // coordinates are adjusted to the widget origin
556 bool onKey (KeyEvent event
) { return false; }
557 bool onMouse (MouseEvent event
) { return false; }
558 bool onChar (dchar ch
) { return false; }
560 // coordinates are adjusted to the widget origin (NOT dest!)
561 bool onKeySink (Widget dest
, KeyEvent event
) { return false; }
562 bool onMouseSink (Widget dest
, MouseEvent event
) { return false; }
563 bool onCharSink (Widget dest
, dchar ch
) { return false; }
565 // coordinates are adjusted to the widget origin (NOT dest!)
566 bool onKeyBubble (Widget dest
, KeyEvent event
) { return false; }
567 bool onMouseBubble (Widget dest
, MouseEvent event
) { return false; }
568 bool onCharBubble (Widget dest
, dchar ch
) { return false; }
572 // all coords are global
573 void drawTextCursor (int x
, int y
, int hgt
=-666) {
575 immutable msecs
= MonoTime
.currTime
.ticks
*1000/MonoTime
.ticksPerSecond
;
576 immutable int btm
= getInt("text-cursor-blink-time", 0);
577 immutable bool ctt
= (btm
>= 20 ?
(msecs
%btm
< btm
/2) : false);
578 immutable uint clr
= getColor(ctt ?
"text-cursor-0" : "text-cursor-1");
580 gxVLine(x
, y
, hgt
, clr
);
582 //gxFillRect(x, y, 2, hgt, clr);
583 gxFillRect(x
, y
+1, 2, hgt
-2, clr
);
584 gxHLine(x
-2, y
, 6, clr
);
585 if (hgt
> 1) gxHLine(x
-2, y
+hgt
-1, 6, clr
);
589 immutable uint clrDot
= getColor("text-cursor-dot");
590 if (hgt
> 2 && !gxIsTransparent(clrDot
)) {
591 ctm
= getInt("text-cursor-dot-time", 0);
594 int doty
= msecs
/ctm
%(hgt
*2-1);
595 if (doty
>= hgt
) doty
= hgt
*2-doty
-1;
597 gxPutPixel(x
, y
+doty
+1, clrDot
);
599 gxHLine(x
, y
+doty
+1, 2, clrDot
);
603 if (ctm
>= 20 || btm
>= 20) {
604 if (!ctm || ctm
> (btm
>>1)) ctm
= (btm
>>1);