egra: prepare agg mini before calling `onPaint()` (and cleanup afterwards)
[iv.d.git] / egra / gui / subwindows.d
blobd6f263345d682b2d6f3423cdb43831114d553eb5
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.subwindows /*is aliced*/;
20 private:
22 import core.time;
24 import arsd.simpledisplay;
26 import iv.alice;
27 import iv.cmdcon;
28 import iv.dynstring;
29 import iv.flexlay2;
30 import iv.strex;
31 import iv.unarray;
32 import iv.utfutil;
34 import iv.egra.gfx;
35 public import iv.egra.gui.style;
36 import iv.egra.gui.widgets : Widget, RootWidget;
39 // ////////////////////////////////////////////////////////////////////////// //
40 public __gshared SimpleWindow vbwin; // main window; MUST be set!
41 public __gshared bool vbfocused = false;
44 // ////////////////////////////////////////////////////////////////////////// //
45 __gshared public int lastMouseXUnscaled = 10000, lastMouseYUnscaled = 10000;
46 __gshared public uint lastMouseButton;
48 public int lastMouseX () nothrow @trusted @nogc { pragma(inline, true); return lastMouseXUnscaled/screenEffScale; }
49 public int lastMouseY () nothrow @trusted @nogc { pragma(inline, true); return lastMouseYUnscaled/screenEffScale; }
51 public bool lastMouseLeft () nothrow @trusted @nogc { pragma(inline, true); return ((lastMouseButton&MouseButton.left) != 0); }
52 public bool lastMouseRight () nothrow @trusted @nogc { pragma(inline, true); return ((lastMouseButton&MouseButton.right) != 0); }
53 public bool lastMouseMiddle () nothrow @trusted @nogc { pragma(inline, true); return ((lastMouseButton&MouseButton.middle) != 0); }
56 // ////////////////////////////////////////////////////////////////////////// //
57 public class ScreenRebuildEvent {}
58 public class ScreenRepaintEvent {}
59 public class QuitEvent {}
60 public class CursorBlinkEvent {}
61 public class HideMouseEvent {}
63 __gshared ScreenRebuildEvent evScrRebuild;
64 __gshared ScreenRepaintEvent evScreenRepaint;
65 __gshared CursorBlinkEvent evCurBlink;
66 __gshared HideMouseEvent evHideMouse;
68 shared static this () {
69 evScrRebuild = new ScreenRebuildEvent();
70 evScreenRepaint = new ScreenRepaintEvent();
71 evCurBlink = new CursorBlinkEvent();
72 evHideMouse = new HideMouseEvent();
76 // ////////////////////////////////////////////////////////////////////////// //
77 public void postScreenRebuild () { if (vbwin !is null && !vbwin.eventQueued!ScreenRebuildEvent) vbwin.postEvent(evScrRebuild); }
78 public void postScreenRepaint () { if (vbwin !is null && !vbwin.eventQueued!ScreenRepaintEvent && !vbwin.eventQueued!ScreenRebuildEvent) vbwin.postEvent(evScreenRepaint); }
79 public void postScreenRepaintDelayed () { if (vbwin !is null && !vbwin.eventQueued!ScreenRepaintEvent && !vbwin.eventQueued!ScreenRebuildEvent) vbwin.postTimeout(evScreenRepaint, 35); }
81 public void postCurBlink (int timeout) {
82 if (timeout < 1) return;
83 if (vbwin !is null && !vbwin.eventQueued!CursorBlinkEvent) {
84 //conwriteln("curblink posted!");
85 if (timeout < 100) timeout = 100;
86 vbwin.postTimeout(evCurBlink, timeout);
87 //vbwin.postTimeout(evCurBlink, 500);
92 // ////////////////////////////////////////////////////////////////////////// //
93 __gshared MonoTime lastMouseMove;
94 __gshared uint MouseHideTime = 3000;
97 shared static this () {
98 conRegVar("mouse_hide_time", "mouse cursor hiding time (in milliseconds); 0 to not hide it",
99 delegate (self) {
100 return MouseHideTime;
102 delegate (self, uint nv) {
103 if (MouseHideTime != nv) {
104 if (MouseHideTime == 0) egraMouseMoved();
105 MouseHideTime = nv;
106 conwriteln("mouse hiding time: ", nv);
113 //==========================================================================
115 // mshtime_dbg
117 //==========================================================================
118 public int mshtime_dbg () {
119 if (MouseHideTime > 0) {
120 auto ctt = MonoTime.currTime;
121 auto mt = (ctt-lastMouseMove).total!"msecs";
122 return cast(int)mt;
123 } else {
124 return 0;
129 //==========================================================================
131 // isMouseVisible
133 //==========================================================================
134 public bool isMouseVisible () {
135 if (MouseHideTime > 0) {
136 auto ctt = MonoTime.currTime;
137 return ((ctt-lastMouseMove).total!"msecs" < MouseHideTime+500);
138 } else {
139 return true;
144 //==========================================================================
146 // mouseAlpha
148 //==========================================================================
149 public float mouseAlpha () {
150 if (MouseHideTime > 0) {
151 auto ctt = MonoTime.currTime;
152 auto msc = (ctt-lastMouseMove).total!"msecs";
153 if (msc >= MouseHideTime+500) return 0.0f;
154 if (msc < MouseHideTime) return 1.0f;
155 msc -= MouseHideTime;
156 return 1.0f-msc/500.0f;
157 } else {
158 return 1.0f;
163 //==========================================================================
165 // repostHideMouse
167 // returns `true` if mouse should be redrawn
169 //==========================================================================
170 public bool repostHideMouse () {
171 if (vbwin is null || vbwin.eventQueued!HideMouseEvent) return false;
172 if (MouseHideTime > 0) {
173 auto ctt = MonoTime.currTime;
174 auto tms = (ctt-lastMouseMove).total!"msecs";
175 if (tms >= MouseHideTime) {
176 if (tms >= MouseHideTime+500) return true; // hide it
177 vbwin.postTimeout(evHideMouse, 50);
178 return true; // fade it
180 vbwin.postTimeout(evHideMouse, cast(int)(MouseHideTime-tms));
182 return false;
186 //==========================================================================
188 // egraMouseMoved
190 //==========================================================================
191 public void egraMouseMoved () {
192 if (MouseHideTime > 0) {
193 lastMouseMove = MonoTime.currTime;
194 if (vbwin !is null && !vbwin.eventQueued!HideMouseEvent) vbwin.postTimeout(evHideMouse, MouseHideTime);
196 if (vArrowTextureId && isMouseVisible) postScreenRepaint();
200 // ////////////////////////////////////////////////////////////////////////// //
201 private __gshared SubWindow subwinLast;
202 private __gshared bool ignoreSubWinChar = false;
203 // make a package and move that to package
204 private __gshared SubWindow subwinDrag = null;
205 private __gshared int subwinDragXSpot, subwinDragYSpot;
206 private __gshared SubWindow lastHoverWindow = null;
209 public @property bool isSubWinDragging () nothrow @trusted @nogc { pragma(inline, true); return (subwinDrag !is null); }
210 public @property bool isSubWinDraggingKeyboard () nothrow @trusted @nogc { pragma(inline, true); return (subwinDrag !is null && subwinDragXSpot == int.min && subwinDragYSpot == int.min); }
213 //==========================================================================
215 // eguiLostGlobalFocus
217 //==========================================================================
218 public void eguiLostGlobalFocus () {
219 ignoreSubWinChar = false;
220 subwinDrag = null;
221 SubWindow aw = getActiveSubWindow();
222 if (aw !is null) aw.releaseWidgetGrab();
223 lastMouseButton = 0;
227 private bool insertSubWindow (SubWindow nw) {
228 if (nw is null || nw.mClosed) return false;
229 assert(nw.mPrev is null);
230 assert(nw.mNext is null);
231 assert(!nw.mInWinList);
232 lastHoverWindow = null; // just in case
233 nw.releaseWidgetGrab();
234 SubWindow law = getActiveSubWindow();
235 if (nw.mType == SubWindow.Type.OnBottom) {
236 SubWindow w = subwinLast;
237 if (w !is null) {
238 while (w.mPrev !is null) w = w.mPrev;
239 nw.mNext = w;
240 if (w !is null) w.mPrev = nw; else subwinLast = nw;
241 } else {
242 subwinLast = nw;
244 nw.mInWinList = true;
245 if (law !is null && law !is getActiveSubWindow()) law.releaseWidgetGrab();
246 return true;
248 SubWindow aw = getActiveSubWindow();
249 assert(aw !is nw);
250 if (nw.mType == SubWindow.Type.OnTop || aw is null) {
251 nw.mPrev = subwinLast;
252 if (subwinLast !is null) subwinLast.mNext = nw;
253 subwinLast = nw;
254 nw.mInWinList = true;
255 if (law !is null && law !is getActiveSubWindow()) law.releaseWidgetGrab();
256 return true;
258 if (aw.mModal && !nw.mModal) return false; // can't insert normal windows while modal window is active
259 // insert after aw
260 nw.mPrev = aw;
261 nw.mNext = aw.mNext;
262 aw.mNext = nw;
263 if (nw.mNext !is null) nw.mNext.mPrev = nw;
264 if (aw is subwinLast) subwinLast = nw;
265 nw.mInWinList = true;
266 if (law !is null && law !is getActiveSubWindow()) law.releaseWidgetGrab();
267 return true;
271 private bool removeSubWindow (SubWindow nw) {
272 if (nw is null || !nw.mInWinList) return false;
273 lastHoverWindow = null; // just in case
274 nw.releaseWidgetGrab();
275 SubWindow law = getActiveSubWindow();
276 if (nw.mPrev !is null) nw.mPrev.mNext = nw.mNext;
277 if (nw.mNext !is null) nw.mNext.mPrev = nw.mPrev;
278 if (nw is subwinLast) subwinLast = nw.mPrev;
279 nw.mPrev = null;
280 nw.mNext = null;
281 nw.mInWinList = false;
282 if (law !is null && law !is getActiveSubWindow()) law.releaseWidgetGrab();
283 return true;
287 //==========================================================================
289 // mouse2xy
291 //==========================================================================
292 public void mouse2xy (MouseEvent event, out int mx, out int my) nothrow @trusted @nogc {
293 mx = event.x/screenEffScale;
294 my = event.y/screenEffScale;
298 //==========================================================================
300 // subWindowAt
302 //==========================================================================
303 public SubWindow subWindowAt (in GxPoint p) nothrow {
304 for (SubWindow w = subwinLast; w !is null; w = w.mPrev) {
305 if (!w.closed) {
306 if (w.minimised) {
307 if (p.x >= w.winminx && p.y >= w.winminy && p.x < w.winminx+w.MinSizeX && p.y < w.winminy+w.MinSizeY) return w;
308 } else {
309 if (p.inside(w.winrect)) return w;
313 return null;
317 public SubWindow subWindowAt (int mx, int my) nothrow @trusted { return subWindowAt(GxPoint(mx, my)); }
320 //==========================================================================
322 // subWindowAt
324 //==========================================================================
325 public SubWindow subWindowAt (MouseEvent event) nothrow @trusted {
326 pragma(inline, true);
327 return subWindowAt(event.x/screenEffScale, event.y/screenEffScale);
331 //==========================================================================
333 // getActiveSubWindow
335 //==========================================================================
336 public SubWindow getActiveSubWindow () nothrow @trusted @nogc {
337 for (SubWindow w = subwinLast; w !is null; w = w.mPrev) {
338 if (!w.mClosed && w.type != SubWindow.Type.OnTop && !w.minimised) return w;
340 return null;
344 //==========================================================================
346 // dispatchEvent
348 //==========================================================================
349 public bool dispatchEvent (KeyEvent event) {
350 bool res = false;
351 if (isSubWinDragging) {
352 if (isSubWinDraggingKeyboard) {
353 if (!event.pressed) return true;
354 if (event == "Left") subwinDrag.x0 = subwinDrag.x0-1;
355 else if (event == "Right") subwinDrag.x0 = subwinDrag.x0+1;
356 else if (event == "Up") subwinDrag.y0 = subwinDrag.y0-1;
357 else if (event == "Down") subwinDrag.y0 = subwinDrag.y0+1;
358 else if (event == "C-Left") subwinDrag.x0 = subwinDrag.x0-8;
359 else if (event == "C-Right") subwinDrag.x0 = subwinDrag.x0+8;
360 else if (event == "C-Up") subwinDrag.y0 = subwinDrag.y0-8;
361 else if (event == "C-Down") subwinDrag.y0 = subwinDrag.y0+8;
362 else if (event == "Home") subwinDrag.x0 = 0;
363 else if (event == "End") subwinDrag.x0 = screenWidth-subwinDrag.width;
364 else if (event == "PageUp") subwinDrag.y0 = 0;
365 else if (event == "PageDown") subwinDrag.y0 = screenHeight-subwinDrag.height;
366 else if (event == "Escape" || event == "Enter") subwinDrag = null;
367 postScreenRebuild();
368 return true;
370 } else if (auto aw = getActiveSubWindow()) {
371 res = aw.onKeyEvent(event);
373 return res;
377 //==========================================================================
379 // dispatchEvent
381 //==========================================================================
382 public bool dispatchEvent (MouseEvent event) {
383 scope(exit) {
384 if (event.type == MouseEventType.buttonPressed) lastMouseButton |= cast(uint)event.button;
385 else if (event.type == MouseEventType.buttonReleased) lastMouseButton &= ~cast(uint)event.button;
388 if (subwinLast is null) {
389 if (lastHoverWindow !is null) {
390 lastHoverWindow = null;
391 postScreenRebuild();
393 return false;
396 int mx = lastMouseX;
397 int my = lastMouseY;
399 // drag
400 if (isSubWinDragging) {
401 assert(subwinDrag !is null);
402 lastHoverWindow = subwinDrag;
403 subwinDrag.releaseWidgetGrab(); // just in case
404 if (!isSubWinDraggingKeyboard) {
405 subwinDrag.x0 = mx+subwinDragXSpot;
406 subwinDrag.y0 = my+subwinDragYSpot;
407 // stop drag?
408 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) subwinDrag = null;
409 postScreenRebuild();
411 return true;
414 SubWindow aw = getActiveSubWindow();
415 immutable bool curIsModal = (aw !is null && aw.mModal);
416 SubWindow msw = (curIsModal || aw.hasGrab ? aw : subWindowAt(event));
417 if (msw != lastHoverWindow) {
418 if ((msw !is null && msw.minimised) || (lastHoverWindow !is null && lastHoverWindow.minimised)) {
419 postScreenRebuild();
421 lastHoverWindow = msw;
424 // switch window by button press
425 if (event.type == MouseEventType.buttonReleased && msw !is aw && !curIsModal) {
426 if (msw !is null && msw.mType == SubWindow.Type.Normal) {
427 if (aw !is null) aw.releaseWidgetGrab();
428 msw.releaseWidgetGrab();
429 msw.bringToFront();
430 return true;
434 if (msw is null || msw !is aw || msw.minimised) {
435 if (msw is null || !msw.onTop) {
436 return false;
439 assert(msw !is null);
441 if (msw.onMouseEvent(event)) {
442 egraMouseMoved();
443 return true;
446 egraMouseMoved();
447 return false;
451 //==========================================================================
453 // dispatchEvent
455 //==========================================================================
456 public bool dispatchEvent (dchar ch) {
457 if (ignoreSubWinChar) { ignoreSubWinChar = false; return (subwinLast !is null); }
458 bool res = false;
459 if (!isSubWinDragging) {
460 if (auto aw = getActiveSubWindow()) {
461 res = aw.onCharEvent(ch);
464 return res;
468 //==========================================================================
470 // paintSubWindows
472 //==========================================================================
473 public void paintSubWindows () {
474 // get first window
475 SubWindow firstWin = subwinLast;
476 if (firstWin is null) return;
477 while (firstWin.mPrev !is null) firstWin = firstWin.mPrev;
479 SubWindow firstMin, firstNormal, firstTop;
480 SubWindow lastMin, lastNormal, lastTop;
482 //gxClipReset();
484 void doDraw (SubWindow w) {
485 if (w !is null) {
486 gxClipReset();
487 w.onPaint();
488 if (w is subwinDrag) w.drawDragRect();
492 // paint background windows
493 for (SubWindow w = firstWin; w !is null; w = w.mNext) {
494 if (w.mClosed) continue;
495 if (w.minimised) {
496 if (firstMin is null) firstMin = w;
497 lastMin = w;
498 } else if (w.mType == SubWindow.Type.Normal) {
499 if (firstNormal is null) firstNormal = w;
500 lastNormal = w;
501 } else if (w.mType == SubWindow.Type.OnTop) {
502 if (firstTop is null) firstTop = w;
503 lastTop = w;
504 } else if (w.mType == SubWindow.Type.OnBottom) {
505 doDraw(w);
509 // paint minimised windows
510 for (SubWindow w = firstMin; w !is null; w = w.mNext) {
511 if (!w.mClosed && w.minimised) doDraw(w);
512 if (w is lastMin) break;
515 // paint normal windows
516 for (SubWindow w = firstNormal; w !is null; w = w.mNext) {
517 if (!w.mClosed && !w.minimised && w.mType == SubWindow.Type.Normal) doDraw(w);
518 if (w is lastNormal) break;
521 // paint ontop windows
522 for (SubWindow w = firstTop; w !is null; w = w.mNext) {
523 if (!w.mClosed && !w.minimised && w.mType == SubWindow.Type.OnTop) doDraw(w);
524 if (w is lastTop) break;
527 // paint hint for minimised window
528 if (auto msw = /*subWindowAt(lastMouseX, lastMouseY)*/lastHoverWindow) {
529 if (!msw.mClosed && msw.minimised && msw.mWinTitle.length) {
530 auto wdt = gxTextWidthUtf(msw.mWinTitle)+2;
531 auto hgt = gxTextHeightUtf+2;
532 int y = msw.winminy-hgt;
533 int x;
534 if (wdt >= screenWidth) {
535 x = (screenWidth-wdt)/2;
536 } else {
537 x = (msw.winminx+msw.MinSizeX)/2-wdt/2;
538 if (x < 0) x = 0;
540 gxClipReset();
541 gxFillRect(x, y, wdt, hgt, gxRGB!(255, 255, 255));
542 gxDrawTextUtf(x+1, y+1, msw.mWinTitle, gxRGB!(0, 0, 0));
548 // ////////////////////////////////////////////////////////////////////////// //
549 public class SubWindow : EgraStyledClass {
550 protected:
551 enum Type {
552 Normal,
553 OnTop,
554 OnBottom,
557 protected:
558 SubWindow mPrev, mNext;
559 Type mType = Type.Normal;
560 bool mMinimised;
561 bool mInWinList;
562 bool mModal;
563 bool mClosed;
564 bool mAllowMinimise = true;
565 bool mAllowDragMove = true;
566 bool mNoTitle = false;
567 RootWidget mRoot;
569 protected:
570 int winminx, winminy;
571 GxRect winrect;
572 dynstring mWinTitle;
574 public:
575 GxSize minWinSize;
576 GxSize maxWinSize;
578 public:
579 override bool isMyModifier (const(char)[] str) nothrow @trusted @nogc {
580 if (str.length == 0) return !active;
581 if (mMinimised) return false;
582 if (strEquCI(str, "focused")) return active;
583 return false;
586 override bool isMyStyleClass (const(char)[] str) nothrow @trusted @nogc {
587 if (mMinimised && str.strEquCI("minimised")) return true;
588 return super.isMyStyleClass(str);
591 override string getCurrentMod () nothrow @trusted @nogc {
592 return (mMinimised ? "" : active ? "focused" : "");
595 override EgraStyledClass getParent () nothrow @trusted @nogc { return null; }
596 override EgraStyledClass getFirstChild () nothrow @trusted @nogc { return mRoot; }
598 override bool isMyClassName (const(char)[] str) nothrow @trusted @nogc {
599 if (str.length == 0) return true;
600 // sorry for this cast
601 for (TypeInfo_Class ti = cast(TypeInfo_Class)typeid(this); ti !is null; ti = ti.base) {
602 if (str.strEquCI(classShortName(ti))) return true;
604 return false;
607 final T querySelector(T:EgraStyledClass=Widget) (const(char)[] sel) { pragma(inline, true); return querySelectorInternal!T(sel); }
608 final auto querySelectorAll(T:EgraStyledClass=Widget) (const(char)[] sel) nothrow @safe @nogc { pragma(inline, true); return Iter!T(this, sel); }
610 protected:
611 void createRoot () {
612 if (mRoot is null) setRoot(new RootWidget(this)); else fixRoot();
615 void fixRoot () {
616 if (mRoot) {
617 mRoot.rect.pos = GxPoint(0, 0);
618 mRoot.rect.size = GxSize(clientWidth, clientHeight);
622 void finishConstruction () {
623 winrect.size.sanitize();
624 createRoot();
625 createStyle();
626 mRoot.enter(&createWidgets);
627 finishCreating();
630 public:
631 this () {
632 winrect.pos = GxPoint(0, 0);
633 winrect.size = GxSize(0, 0);
634 finishConstruction();
637 this (const(char)[] atitle) {
638 caption = atitle;
639 winrect.pos = GxPoint(0, 0);
640 winrect.size = GxSize(0, 0);
641 finishConstruction();
644 this (const(char)[] atitle, in GxPoint apos, in GxSize asize) {
645 caption = atitle;
646 winrect.pos = apos;
647 winrect.size = asize;
648 finishConstruction();
651 this (const(char)[] atitle, in GxSize asize) {
652 caption = atitle;
653 winrect.size = asize;
654 winrect.size.sanitize();
655 winrect.pos.x = (screenWidth-winrect.width)/2;
656 winrect.pos.y = (screenHeight-winrect.height)/2;
657 finishConstruction();
660 @property void caption (const(char)[] atitle) {
661 if (atitle is null) {
662 mWinTitle.clear();
663 mNoTitle = true;
664 } else {
665 mWinTitle = atitle;
666 mNoTitle = false;
670 @property dynstring caption () const nothrow @nogc { return dynstring(mWinTitle); }
672 bool hasGrab () {
673 return (mRoot !is null ? mRoot.hasGrab() : false);
676 override void widgetChanged () nothrow {
677 if (mInWinList && !minimised) {
678 try {
679 postScreenRebuild();
680 } catch (Exception e) {
681 // sorry
686 // this doesn't perform relayouting
687 void setRoot (RootWidget w) {
688 if (mRoot !is w) {
689 mRoot = w;
690 fixRoot();
691 widgetChanged();
695 // this is called from constructor
696 void createStyle () {
699 // this is called from constructor
700 void createWidgets () {
703 // this is called from constructor
704 // you can call `addModal()` here, for example
705 void finishCreating () {
706 add();
709 void relayout (bool resizeWindow=false) {
710 if (mRoot is null) return;
712 FuiFlexLayouter!Widget lay;
714 lay.isValidBoxId = delegate bool (Widget id) { return (id !is null); };
716 lay.firstChild = delegate Widget (Widget id) { return id.firstChild; };
717 lay.nextSibling = delegate Widget (Widget id) { return id.nextSibling; };
719 lay.getMinSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.minSize.w : id.minSize.h); };
720 lay.getMaxSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.maxSize.w : id.maxSize.h); };
721 lay.getPrefSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.prefSize.w : id.prefSize.h); };
723 lay.isHorizBox = delegate bool (Widget id) { return (id.childDir == GxDir.Horiz); };
725 lay.getFlex = delegate int (Widget id) { return id.flex; };
727 lay.getSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.boxsize.w : id.boxsize.h); };
728 lay.setSize = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.boxsize.w = val; else id.boxsize.h = val; };
730 lay.getFinalSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.finalSize.w : id.finalSize.h); };
731 lay.setFinalSize = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.finalSize.w = val; else id.finalSize.h = val; };
733 lay.setFinalPos = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.finalPos.x = val; else id.finalPos.y = val; };
735 if (winrect.size.empty) {
736 if (maxWinSize.empty) maxWinSize = GxSize(screenWidth-16-decorationSizeX, screenHeight-16-decorationSizeY);
737 resizeWindow = true;
740 if (winrect.size.w > screenWidth) winrect.size.w = screenWidth;
741 if (winrect.size.h > screenHeight) winrect.size.h = screenHeight;
743 if (resizeWindow) {
744 mRoot.maxSize = maxWinSize;
745 mRoot.minSize = minWinSize;
746 if (mRoot.minSize.w > screenWidth-decorationSizeX) mRoot.minSize.w = screenWidth-decorationSizeX;
747 if (mRoot.minSize.h > screenHeight-decorationSizeY) mRoot.minSize.h = screenHeight-decorationSizeY;
748 } else {
749 // cannot resize window, so set fixed sizes
750 mRoot.maxSize = winrect.size-GxSize(decorationSizeX, decorationSizeY);
751 if (mRoot.maxSize.w <= 0) mRoot.maxSize.w = maxWinSize.w;
752 if (mRoot.maxSize.h <= 0) mRoot.maxSize.h = maxWinSize.h;
753 mRoot.minSize = mRoot.maxSize;
754 mRoot.prefSize = mRoot.maxSize;
755 mRoot.boxsize = mRoot.maxSize;
758 if (!mNoTitle && mWinTitle.length) {
759 if (mRoot.boxsize.w <= 0) mRoot.boxsize.w = gxTextWidthUtf(mWinTitle)+2;
761 mRoot.prefSize = mRoot.boxsize;
763 Widget[] hsizes;
764 Widget[] vsizes;
765 scope(exit) {
766 hsizes.unsafeArrayClear();
767 delete hsizes;
768 vsizes.unsafeArrayClear();
769 delete vsizes;
772 foreach (Widget w; mRoot.allDepth) {
773 w.preLayout();
774 // hsize
775 w.hsizeIdNext = null;
776 if (w.hsizeId.length) {
777 foreach (ref Widget nw; hsizes) {
778 if (nw.hsizeId == w.hsizeId) {
779 w.hsizeIdNext = nw;
780 nw = w;
781 break;
784 if (w.hsizeIdNext is null) hsizes.unsafeArrayAppend(w);
786 // vsize
787 w.vsizeIdNext = null;
788 if (w.vsizeId.length) {
789 foreach (ref Widget nw; vsizes) {
790 if (nw.vsizeId == w.vsizeId) {
791 w.vsizeIdNext = nw;
792 nw = w;
793 break;
796 if (w.vsizeIdNext is null) vsizes.unsafeArrayAppend(w);
800 // fix horiz pref sizes
801 foreach (Widget sw; hsizes) {
802 int vmax = 0;
803 for (Widget w = sw; w !is null; w = w.hsizeIdNext) if (vmax < w.prefSize.w) vmax = w.prefSize.w;
804 for (Widget w = sw; w !is null; w = w.hsizeIdNext) w.prefSize.w = vmax;
807 // fix vert pref sizes
808 foreach (Widget sw; vsizes) {
809 int vmax = 0;
810 for (Widget w = sw; w !is null; w = w.vsizeIdNext) if (vmax < w.prefSize.h) vmax = w.prefSize.h;
811 for (Widget w = sw; w !is null; w = w.vsizeIdNext) w.prefSize.h = vmax;
814 lay.layout(mRoot);
816 winrect.size = mRoot.boxsize+GxSize(decorationSizeX, decorationSizeY);
817 if (winrect.x1 >= screenWidth) winrect.pos.x -= winrect.x1-screenWidth+1;
818 if (winrect.y1 >= screenHeight) winrect.pos.y -= winrect.y1-screenHeight+1;
819 if (winrect.pos.x < 0) winrect.pos.x = 0;
820 if (winrect.pos.y < 0) winrect.pos.y = 0;
822 foreach (Widget w; mRoot.allDepth) {
823 w.hsizeIdNext = null;
824 w.vsizeIdNext = null;
825 w.postLayout();
827 widgetChanged();
830 void relayoutResize () { relayout(true); }
832 final @property Widget rootWidget () pure nothrow @safe @nogc { pragma(inline, true); return mRoot; }
834 final @property int x0 () nothrow @safe { return (minimised ? winminx : winrect.pos.x); }
835 final @property int y0 () nothrow @safe { return (minimised ? winminy : winrect.pos.y); }
836 final @property int width () nothrow @safe { return (minimised ? MinSizeX : winrect.size.w); }
837 final @property int height () nothrow @safe { return (minimised ? MinSizeY : winrect.size.h); }
839 final @property void x0 (in int v) {
840 if (minimised) {
841 if (winminx != v) { winminx = v; if (mInWinList) postScreenRebuild(); }
842 } else {
843 if (winrect.pos.x != v) { winrect.pos.x = v; widgetChanged(); }
846 final @property void y0 (in int v) {
847 if (minimised) {
848 if (winminy != v) { winminy = v; if (mInWinList) postScreenRebuild(); }
849 } else {
850 if (winrect.pos.y != v) { winrect.pos.y = v; widgetChanged(); }
853 final @property void width (in int v) { pragma(inline, true); setSize(v, winrect.size.h); }
854 final @property void height (in int v) { pragma(inline, true); setSize(winrect.size.w, v); }
856 final void setPos (in int newx, in int newy) {
857 if (winrect.pos.x != newx || winrect.pos.y != newy) {
858 winrect.pos.x = newx;
859 winrect.pos.y = newy;
860 widgetChanged();
864 final void setSize (in int awidth, in int aheight) {
865 bool changed = false;
866 if (awidth > 0 && awidth != winrect.size.w) {
867 changed = true;
868 immutable bool chsz = (mRoot !is null && mRoot.width == clientWidth);
869 winrect.size.w = awidth;
870 if (chsz || mRoot.width > clientWidth) mRoot.width = clientWidth;
872 if (aheight > 0 && aheight != winrect.size.w) {
873 changed = true;
874 immutable bool chsz = (mRoot !is null && mRoot.height == clientHeight);
875 winrect.size.h = aheight;
876 if (chsz || mRoot.height > clientHeight) mRoot.height = clientHeight;
878 if (changed) {
879 //fixRoot();
880 widgetChanged();
884 final void setClientSize (int awidth, int aheight) {
885 setSize((awidth > 0 ? awidth+decorationSizeX : awidth), (aheight > 0 ? aheight+decorationSizeY : aheight));
888 final void centerWindow () {
889 immutable int newx = (screenWidth-winrect.size.w)/2;
890 immutable int newy = (screenHeight-winrect.size.h)/2;
891 if (winrect.pos.x != newx || winrect.pos.y != newy) {
892 winrect.pos.x = newx;
893 winrect.pos.y = newy;
894 widgetChanged();
898 final @property SubWindow prev () pure nothrow @safe @nogc { return mPrev; }
899 final @property SubWindow next () pure nothrow @safe @nogc { return mNext; }
901 final @property Type type () const pure nothrow @safe @nogc { return mType; }
902 final @property bool onTop () const pure nothrow @safe @nogc { return (mType == Type.OnTop); }
903 final @property bool onBottom () const pure nothrow @safe @nogc { return (mType == Type.OnBottom); }
905 final @property bool inWinList () const pure nothrow @safe @nogc { return mInWinList; }
907 final @property bool modal () const pure nothrow @safe @nogc { return mModal; }
908 final @property bool closed () const pure nothrow @safe @nogc { return mClosed; }
910 final @property bool inWindowList () const pure nothrow @safe @nogc { return mInWinList; }
912 final @property bool active () const nothrow @trusted @nogc {
913 if (!mInWinList || mClosed || minimised) return false;
914 return (getActiveSubWindow is this);
917 @property int decorationSizeX () const nothrow @safe { return 2*2; }
918 @property int decorationSizeY () const nothrow @safe { immutable hgt = gxTextHeightUtf; return (hgt < 10 ? 10 : hgt+1)+4; }
920 @property int clientOffsetX () const nothrow @safe { return 2; }
921 @property int clientOffsetY () const nothrow @safe { immutable hgt = gxTextHeightUtf; return (hgt < 10 ? 10 : hgt+1)+2; }
923 final @property int clientWidth () const nothrow @safe { pragma(inline, true); return winrect.size.w-decorationSizeX; }
924 final @property int clientHeight () const nothrow @safe { pragma(inline, true); return winrect.size.h-decorationSizeY; }
926 final @property void clientWidth (in int v) { pragma(inline, true); setClientSize(v, clientHeight); }
927 final @property void clientHeight (in int v) { pragma(inline, true); setClientSize(clientWidth, v); }
929 final @property Widget focusedWidget () {
930 if (mRoot is null) return null;
931 Widget w = mRoot.focusedWidget;
932 return (w !is null && w.canAcceptFocus ? w : null);
935 protected void drawWidgets () {
936 setupClientClip();
937 if (mRoot !is null) mRoot.onPaint();
940 // draw window frame and background in "normal" state
941 protected void drawNormalDecoration () {
942 //immutable string act = (active ? null : "inactive");
943 gxDrawWindow(winrect, (mNoTitle ? null : mWinTitle.length ? mWinTitle.getData : ""),
944 getColor("frame"),
945 getColor("title-text"),
946 getColor("title-back"),
947 getColor("back"),
948 getColor("shadow-color"),
949 getInt("shadow-size", 0),
950 (getInt("shadow-dash", 0) > 0));
953 // draw window frame and background in "minimised" state
954 protected void drawWindowMinimised () {
955 //immutable string act = (active ? null : "inactive");
956 gxFillRect(winminx, winminy, MinSizeX, MinSizeY, getColor("back"));
957 gxDrawRect(winminx, winminy, MinSizeX, MinSizeY, getColor("frame"));
960 // this changes clip
961 protected void drawDecoration () {
962 if (minimised) {
963 if (gxClipRect.intersect(GxRect(winminx, winminy, MinSizeX, MinSizeY))) drawWindowMinimised();
964 } else {
965 setupClip();
966 drawNormalDecoration();
970 protected void drawDragRect () {
971 immutable uint clr = getColor("drag-overlay-back");
972 if (gxIsTransparent(clr)) return;
973 immutable bool dashed = (getInt("drag-overlay-dash", 0) > 0);
974 gxWithSavedClip {
975 gxClipReset();
976 if (dashed) {
977 gxDashRect(x0, y0, width, height, clr);
978 } else {
979 gxFillRect(x0, y0, width, height, clr);
985 void releaseWidgetGrab () {
986 if (mRoot !is null) mRoot.releaseGrab();
989 // event in our local coords
990 bool startMouseDrag (MouseEvent event) {
991 if (!mAllowDragMove || !mInWinList) return false;
992 releaseWidgetGrab();
993 subwinDrag = this;
994 subwinDragXSpot = -event.x;
995 subwinDragYSpot = -event.y;
996 widgetChanged();
997 return true;
1000 bool startKeyboardDrag () {
1001 if (!mAllowDragMove || !mInWinList) return false;
1002 releaseWidgetGrab();
1003 subwinDrag = this;
1004 subwinDragXSpot = int.min;
1005 subwinDragYSpot = int.min;
1006 widgetChanged();
1007 return true;
1010 void stopDrag () {
1011 if (subwinDrag is this) {
1012 releaseWidgetGrab();
1013 subwinDrag = null;
1014 widgetChanged();
1018 void onPaint () {
1019 if (closed) return;
1020 gxWithSavedClip {
1021 gxagg.beginFrame();
1022 scope(exit) gxagg.cancelFrame();
1023 drawDecoration();
1024 if (!minimised) drawWidgets();
1029 bool onKeySink (KeyEvent event) {
1030 return false;
1033 bool onKeyBubble (KeyEvent event) {
1034 // global window hotkeys
1035 if (event.pressed) {
1036 if (event == "C-F5") { if (startKeyboardDrag()) return true; }
1037 if (/*event == "M-M" ||*/ event == "M-S-M") { if (minimise()) return true; }
1039 return false;
1042 bool onKeyEvent (KeyEvent event) {
1043 if (closed) return false;
1044 if (minimised) return false;
1045 if (onKeySink(event)) return true;
1046 if (mRoot !is null && mRoot.dispatchKey(event)) return true;
1047 return onKeyBubble(event);
1051 bool onMouseSink (MouseEvent event) {
1052 // start drag?
1053 if (subwinDrag is null && event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1054 if (event.x >= 0 && event.y >= 0 &&
1055 event.x < width && event.y < (!minimised ? gxTextHeightUtf+2 : height))
1057 startMouseDrag(event);
1058 return true;
1062 if (minimised) return false;
1064 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.right) {
1065 if (event.x >= 0 && event.y >= 0 &&
1066 event.x < winrect.size.w && event.y < gxTextHeightUtf)
1068 minimise();
1069 return true;
1073 return false;
1076 bool onMouseBubble (MouseEvent event) {
1077 return false;
1080 bool onMouseEvent (MouseEvent event) {
1081 if (!active) return false;
1082 if (closed) return false;
1084 int mx, my;
1085 event.mouse2xy(mx, my);
1087 MouseEvent ev = event;
1088 ev.x = mx-x0;
1089 ev.y = my-y0;
1090 if (onMouseSink(ev)) return true;
1092 if (mRoot !is null) {
1093 ev = event;
1094 ev.x = mx-(x0+clientOffsetX)-mRoot.rect.x0;
1095 ev.y = my-(y0+clientOffsetY)-mRoot.rect.y0;
1096 if (mRoot.dispatchMouse(ev)) return true;
1099 ev = event;
1100 ev.x = mx-x0;
1101 ev.y = my-y0;
1102 return onMouseBubble(ev);
1106 bool onCharSink (dchar ch) {
1107 return false;
1110 bool onCharBubble (dchar ch) {
1111 return false;
1114 bool onCharEvent (dchar ch) {
1115 if (!active) return false;
1116 if (closed) return false;
1117 if (minimised) return false;
1118 if (onCharSink(ch)) return true;
1119 if (mRoot !is null && mRoot.dispatchChar(ch)) return true;
1120 return onCharBubble(ch);
1123 void setupClip () {
1124 gxClipRect.intersect(winrect);
1127 final void setupClientClip () {
1128 setupClip();
1129 gxClipRect.intersect(GxRect(
1130 GxPoint(winrect.pos.x+clientOffsetX, winrect.pos.y+clientOffsetY),
1131 GxPoint(winrect.pos.x+clientOffsetX+clientWidth-1, winrect.pos.y+clientOffsetY+clientHeight-1)));
1134 void close () {
1135 if (!mClosed) {
1136 mClosed = true;
1137 if (removeSubWindow(this)) postScreenRebuild();
1141 protected bool addToSubwinList (bool asModal, bool fromKeyboard) {
1142 if (fromKeyboard) ignoreSubWinChar = true;
1143 if (mInWinList) return true;
1144 mModal = asModal;
1145 if (insertSubWindow(this)) {
1146 if (mRoot !is null) mRoot.fixFocus();
1147 widgetChanged();
1148 postScreenRebuild();
1149 return true;
1151 return false;
1154 void add (bool fromKeyboard=false) { addToSubwinList(false, fromKeyboard); }
1156 void addModal (bool fromKeyboard=false) { addToSubwinList(true, fromKeyboard); }
1158 // return `false` to reject
1159 protected bool minimiseQuery () {
1160 return true;
1163 // return `false` to reject (this is "unminimise")
1164 protected bool restoreQuery () {
1165 return true;
1168 void bringToFront () {
1169 if (mClosed || !mInWinList) return;
1170 if (minimised && !restoreQuery()) return;
1171 auto aw = getActiveSubWindow();
1172 if (aw is this) {
1173 if (minimised) {
1174 mMinimised = false;
1175 lastHoverWindow = null;
1176 widgetChanged();
1178 return;
1180 if (aw !is null && aw.mModal) return; // alas
1181 removeSubWindow(this);
1182 mMinimised = false;
1183 insertSubWindow(this);
1184 if (subwinDrag !is this) subwinDrag = null;
1185 widgetChanged();
1188 final @property bool allowMinimise () pure const nothrow @safe @nogc { return mAllowMinimise; }
1189 @property void allowMinimise (in bool v) { mAllowMinimise = v; }
1191 final @property bool allowDragMove () pure const nothrow @safe @nogc { return mAllowDragMove; }
1192 @property void allowDragMove (in bool v) { mAllowDragMove = v; }
1194 final @property bool minimised () pure const nothrow @safe @nogc { return mMinimised; }
1195 @property void minimised (in bool v) {
1196 if (v == mMinimised) return;
1197 if (!mAllowMinimise && v) return;
1198 if (v) minimise(); else bringToFront();
1201 bool minimise () {
1202 if (minimised) return true;
1203 if (mClosed || !mAllowMinimise || mModal) return false;
1204 if (!mInWinList) {
1205 if (minimiseQuery()) mMinimised = true;
1206 return true;
1208 assert(subwinLast !is null);
1209 releaseWidgetGrab();
1210 findMinimisedPos(winminx, winminy);
1211 auto aw = getActiveSubWindow();
1212 if (aw is this) subwinDrag = null;
1213 mMinimised = true;
1214 lastHoverWindow = null;
1215 postScreenRebuild(); // because minimised widgets will not post this
1216 return true;
1219 void restore () {
1220 releaseWidgetGrab();
1221 bringToFront();
1224 public:
1225 static T clampval(T) (in T val, in T min, in T max) pure nothrow @safe @nogc {
1226 pragma(inline, true);
1227 return (val < min ? min : val > max ? max : val);
1230 int MinSizeX () nothrow @safe { immutable bool omm = mMinimised; mMinimised = true; scope(exit) mMinimised = omm; return clampval(getInt("icon-size-x", 16), 16, 64); }
1231 int MinSizeY () nothrow @safe { immutable bool omm = mMinimised; mMinimised = true; scope(exit) mMinimised = omm; return clampval(getInt("icon-size-y", 16), 16, 64); }
1233 int MinMarginX () nothrow @safe { immutable bool omm = mMinimised; mMinimised = true; scope(exit) mMinimised = omm; return clampval(getInt("icon-margin-x", 3), 1, 16); }
1234 int MinMarginY () nothrow @safe { immutable bool omm = mMinimised; mMinimised = true; scope(exit) mMinimised = omm; return clampval(getInt("icon-margin-y", 3), 1, 16); }
1236 protected:
1237 void findMinimisedPos (out int wx, out int wy) {
1238 static bool isOccupied (int x, int y) {
1239 for (SubWindow w = subwinLast; w !is null; w = w.mPrev) {
1240 if (w.mInWinList && !w.closed && w.minimised) {
1241 if (x >= w.winminx && y >= w.winminy && x < w.winminx+w.MinSizeX && y < w.winminy+w.MinSizeY) return true;
1244 return false;
1247 int txcount = screenWidth/(MinSizeX+MinMarginX);
1248 //int tycount = screenHeight/(MinSizeY+MinMarginY);
1249 if (txcount < 1) txcount = 1;
1250 //if (tycount < 1) tycount = 1;
1251 foreach (immutable int n; 0..6/*5535*/) {
1252 int x = (n%txcount)*(MinSizeX+MinMarginX)+1;
1253 int y = screenHeight-MinSizeY-(n/txcount)*(MinSizeY+MinMarginY);
1254 //conwriteln("trying (", x, ",", y, ")");
1255 if (!isOccupied(x, y)) { wx = x; wy = y; /*conwriteln(" HIT!");*/ return; }