egra: splitted widgets to package with separate submodules
[iv.d.git] / egra / gui / widgets / base.d
blob477189c12715bc93e2b0ebede5c657c80abfbef5
1 /*
2 * Simple Framebuffer Gfx/GUI lib
4 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
5 * Understanding is not required. Only obedience.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, version 3 of the License ONLY.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 module iv.egra.gui.widgets.base /*is aliced*/;
20 private:
22 import core.time;
24 import arsd.simpledisplay;
26 import iv.egra.gfx;
28 import iv.alice;
29 import iv.cmdcon;
30 import iv.dynstring;
31 import iv.strex;
32 import iv.utfutil;
34 import iv.egra.gui.subwindows;
37 // ////////////////////////////////////////////////////////////////////////// //
38 // this is used as parent if parent is null (but not for root widgets)
39 public Widget creatorCurrentParent = null;
42 // ////////////////////////////////////////////////////////////////////////// //
43 public enum WidgetStringPropertyMixin(string propname, string fieldName) = `
44 @property dynstring `~propname~` () const nothrow @safe @nogc { return `~fieldName~`; }
45 @property void `~propname~`(T:const(char)[]) (T v) nothrow @trusted {
46 static if (is(T == typeof(null))) {
47 `~fieldName~`.clear();
48 } else {
49 `~fieldName~` = v;
55 // ////////////////////////////////////////////////////////////////////////// //
56 public abstract class Widget : EgraStyledClass {
57 public:
58 static template isGoodEnterBoxDelegate(DG) {
59 import std.traits;
60 enum isGoodEnterBoxDelegate =
61 is(ReturnType!DG == void) &&
62 (is(typeof((inout int=0) { DG dg = void; Widget w; dg(w); })) ||
63 is(typeof((inout int=0) { DG dg = void; dg(); })));
66 static template isEnterBoxDelegateWithArgs(DG) {
67 enum isEnterBoxDelegateWithArgs =
68 isGoodEnterBoxDelegate!DG &&
69 is(typeof((inout int=0) { DG dg = void; Widget w; dg(w); }));
72 public:
73 Widget parent;
74 Widget firstChild;
75 Widget nextSibling;
77 GxRect rect; // relative to parent
78 bool tabStop;
79 // set this to `true` to skip painting; useful for spring and spacer widgets
80 // such widgets will not receive any events too (including sink/bubble)
81 // note that there is no reason to add children to such widgets, because
82 // non-visual widgets cannot be painted, and cannot be focused
83 bool nonVisual;
85 // this is for flexbox layouter
86 enum BoxHAlign {
87 Expand,
88 Left,
89 Center,
90 Right,
93 enum BoxVAlign {
94 Expand,
95 Top,
96 Center,
97 Bottom,
100 GxSize minSize; // minimum size
101 GxSize maxSize = GxSize(int.max, int.max); // maximum size
102 GxSize prefSize = GxSize(int.min, int.min); // preferred (initial) size; will be taken from widget size
104 GxSize boxsize; /// box size
105 GxSize finalSize; /// final size (can be bigger than `size)`
106 GxPoint finalPos; /// final position (for the final size); relative to the parent origin
108 GxDir childDir = GxDir.Horiz; /// children orientation
109 int flex; /// <=0: not flexible
111 BoxHAlign boxHAlign = BoxHAlign.Expand;
112 BoxVAlign boxVAlign = BoxVAlign.Expand;
114 // widgets with the same id will start with the equal preferred sizes (max of all)
115 string hsizeId;
116 string vsizeId;
118 Widget hsizeIdNext;
119 Widget vsizeIdNext;
121 public:
122 // for layouter
123 void preLayout () {
124 if (prefSize.w == int.min) prefSize = rect.size; else rect.size = prefSize;
127 void postLayout () {
128 rect.pos = finalPos;
129 rect.size = boxsize;
130 final switch (boxHAlign) with (BoxHAlign) {
131 case Expand: rect.size.w = finalSize.w; break;
132 case Left: break;
133 case Center: rect.pos.x = finalPos.x+(finalSize.w-boxsize.w)/2; break;
134 case Right: rect.pos.x = finalPos.x+finalSize.w-boxsize.w; break;
136 final switch (boxVAlign) with (BoxVAlign) {
137 case Expand: rect.size.h = finalSize.h; break;
138 case Top: break;
139 case Center: rect.pos.y = finalPos.y+(finalSize.h-boxsize.h)/2; break;
140 case Bottom: rect.pos.y = finalPos.y+finalSize.h-boxsize.h; break;
144 protected:
145 Widget mActive; // do not change directly!
146 dynstring mHotkey;
148 public:
149 bool isMyHotkey (KeyEvent event) {
150 return
151 (mHotkey.length && event == mHotkey) ||
152 (onCheckHotkey !is null && onCheckHotkey(this, event));
155 // will be called if `isMyHotkey()` returned `true`
156 // return `true` if some action was done
157 bool hotkeyActivated () {
158 if (!canAcceptFocus) return false;
159 focus();
160 doAction();
161 return true;
164 public:
165 override bool isMyModifier (const(char)[] str) nothrow @trusted @nogc {
166 //if (nonVisual) return strEquCI("nonvisual");
167 if (str.length == 0) return true;
168 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: MOD: <%.*s>; focused=%d\n", cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(uint)str.length, str.ptr, cast(int)isFocused); }
169 if (strEquCI(str, "focused")) return isFocusedForStyle;
170 if (strEquCI(str, "unfocused")) return !isFocusedForStyle;
171 return false;
174 override EgraStyledClass getFirstChild () nothrow @trusted @nogc { return firstChild; }
175 override EgraStyledClass getNextSibling () nothrow @trusted @nogc { return nextSibling; }
176 override EgraStyledClass getParent () nothrow @trusted @nogc { return parent; }
178 override string getCurrentMod () nothrow @trusted @nogc {
179 return (isFocusedForStyle ? "focused" : "");
182 override void widgetChanged () nothrow {
183 EgraStyledClass w = getTopLevel();
184 if (w is null) return;
185 SubWindow win = cast(SubWindow)w;
186 if (win is null) return;
187 win.widgetChanged();
190 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); }
193 public:
194 dynstring getHotkey () { return mHotkey; }
195 void setHotkey (const(char)[] v) { mHotkey = v; }
197 bool delegate (Widget self, KeyEvent event) onCheckHotkey;
199 public:
200 void delegate (Widget self) onAction;
202 protected void doDefaultAction () {}
204 void doAction () {
205 if (onAction !is null) onAction(this); else doDefaultAction();
206 widgetChanged();
209 public:
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..$];
222 } else {
223 return false;
225 hotxpos = gxTextWidthUtf(text[0..umpos]);
226 hotxlen = gxTextWidthUtf(hotch);
227 hotch = "M-"~hotch;
228 return true;
231 public:
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;
235 assert (w !is this);
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");
239 w.parent = this;
240 Widget lc = firstChild;
241 if (lc is null) {
242 firstChild = w;
243 } else {
244 while (lc.nextSibling) lc = lc.nextSibling;
245 lc.nextSibling = w;
247 w.invalidatePathCache();
248 w.widgetChanged();
249 widgetChanged();
250 //if (mActive is null && w.canAcceptFocus(w)) mActive = w;
253 Widget rootWidget () nothrow {
254 Widget res = this;
255 while (res.parent) res = res.parent;
256 return res;
259 static template isFEDGoodDelegate(DG) {
260 import std.traits;
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) {
268 import std.traits;
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) {
279 return dg(this);
280 } else {
281 dg(this);
282 return null;
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) {
290 if (!w.nonVisual) {
291 Widget res = w.forEachVisualDepth(dg);
292 if (res !is null) return res;
295 static if (isFEDResDelegate!DG) {
296 return dg(this);
297 } else {
298 dg(this);
299 return null;
303 static template isGoodIteratorDelegate(DG) {
304 import std.traits;
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 {
311 Widget w;
312 bool onlyVisual;
314 this (Widget ww, in bool onlyvis) nothrow @safe @nogc {
315 pragma(inline, true);
316 w = ww;
317 onlyVisual = onlyvis;
320 int opApply(DG) (scope DG dg) if (isGoodIteratorDelegate!DG) {
321 int res = 0;
322 if (w is null || dg is null) return 0;
323 if (onlyVisual) {
324 w.forEachVisualDepth((Widget w) {
325 res = dg(w);
326 return (res ? w : null);
328 } else {
329 w.forEachDepth((Widget w) {
330 res = dg(w);
331 return (res ? w : null);
334 return res;
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); }
342 public:
343 this () {
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);
354 // box hierarchy
355 Widget enter(DG) (scope DG dg) if (isGoodEnterBoxDelegate!DG) {
356 if (dg !is null) {
357 Widget oldCCP = creatorCurrentParent;
358 creatorCurrentParent = this;
359 scope(exit) creatorCurrentParent = oldCCP;
360 static if (isEnterBoxDelegateWithArgs!DG) {
361 dg(this);
362 } else {
363 dg();
366 return this;
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);
378 return
379 w is null ? false :
380 w is this ? true :
381 isMyChild(w.parent);
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();
388 return true;
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();
395 return false;
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;
411 Widget res = this;
412 while (res.mActive !is null) res = res.mActive;
413 return res;
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;
423 widgetChanged();
424 releaseGrab();
425 oldFocus.onBlur();
426 Widget w = this;
427 while (w.parent !is null) {
428 w.parent.mActive = w;
429 w = w.parent;
431 onFocus();
432 return true;
435 @property void setTabStop (in bool v) nothrow {
436 if (tabStop == v) return;
437 tabStop = v;
438 if (!v && isFocused) {
439 RootWidget rw = cast(RootWidget)rootWidget;
440 if (rw !is null) {
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;
501 Widget bestChild;
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) {}
525 void onPaint () {
526 if (nonVisual || rect.empty) return; // nothing to paint here
527 if (gxClipRect.empty) return; // totally clipped away
528 gxWithSavedClip {
529 immutable GxRect grect = globalRect;
530 if (!gxClipRect.intersect(grect)) return;
531 // before-children painter
532 gxWithSavedClip { doPaint(grect); };
533 // paint children
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
549 void onBlur () {}
551 // called after setting the focus
552 void onFocus () {}
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; }
570 public:
571 // drawing utilities
572 // all coords are global
573 void drawTextCursor (int x, int y, int hgt=-666) {
574 if (hgt < 1) return;
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");
579 version(none) {
580 gxVLine(x, y, hgt, clr);
581 } else {
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);
587 if (isFocused) {
588 int ctm = 0;
589 immutable uint clrDot = getColor("text-cursor-dot");
590 if (hgt > 2 && !gxIsTransparent(clrDot)) {
591 ctm = getInt("text-cursor-dot-time", 0);
592 if (ctm >= 20) {
593 hgt -= 2;
594 int doty = msecs/ctm%(hgt*2-1);
595 if (doty >= hgt) doty = hgt*2-doty-1;
596 version(none) {
597 gxPutPixel(x, y+doty+1, clrDot);
598 } else {
599 gxHLine(x, y+doty+1, 2, clrDot);
603 if (ctm >= 20 || btm >= 20) {
604 if (!ctm || ctm > (btm>>1)) ctm = (btm>>1);
605 postCurBlink(ctm);