erga: use `widgetChanged()` instead of direct screen rebuild posting; fixes to editor...
[iv.d.git] / egra / gui / subwindows.d
blobce92d63a7310443ba4451c277357c42dcc8aeac5
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 protected:
608 void createRoot () {
609 if (mRoot is null) setRoot(new RootWidget(this)); else fixRoot();
612 void fixRoot () {
613 if (mRoot) {
614 mRoot.rect.pos = GxPoint(0, 0);
615 mRoot.rect.size = GxSize(clientWidth, clientHeight);
619 void finishConstruction () {
620 winrect.size.sanitize();
621 createRoot();
622 createStyle();
623 mRoot.enter(&createWidgets);
624 finishCreating();
627 protected:
628 void setTitleFrom(T:const(char)[]) (T atitle) {
629 static if (is(T == typeof(null))) {
630 mWinTitle.clear();
631 mNoTitle = true;
632 } else {
633 mWinTitle = atitle;
634 mNoTitle = false;
638 public:
639 this () {
640 winrect.pos = GxPoint(0, 0);
641 winrect.size = GxSize(0, 0);
642 finishConstruction();
645 this(T:const(char)[]) (T atitle) {
646 setTitleFrom(atitle);
647 winrect.pos = GxPoint(0, 0);
648 winrect.size = GxSize(0, 0);
649 finishConstruction();
652 this(T:const(char)[]) (T atitle, in GxPoint apos, in GxSize asize) {
653 setTitleFrom(atitle);
654 winrect.pos = apos;
655 winrect.size = asize;
656 finishConstruction();
659 this(T:const(char)[]) (T atitle, in GxSize asize) {
660 setTitleFrom(atitle);
661 winrect.size = asize;
662 winrect.size.sanitize();
663 winrect.pos.x = (screenWidth-winrect.width)/2;
664 winrect.pos.y = (screenHeight-winrect.height)/2;
665 finishConstruction();
668 bool hasGrab () {
669 return (mRoot !is null ? mRoot.hasGrab() : false);
672 override void widgetChanged () nothrow {
673 if (mInWinList && !minimised) {
674 try {
675 postScreenRebuild();
676 } catch (Exception e) {
677 // sorry
682 // this doesn't perform relayouting
683 void setRoot (RootWidget w) {
684 if (mRoot !is w) {
685 mRoot = w;
686 fixRoot();
687 widgetChanged();
691 // this is called from constructor
692 void createStyle () {
695 // this is called from constructor
696 void createWidgets () {
699 // this is called from constructor
700 // you can call `addModal()` here, for example
701 void finishCreating () {
702 add();
705 void relayout (bool resizeWindow) {
706 if (mRoot is null) return;
708 FuiFlexLayouter!Widget lay;
710 lay.isValidBoxId = delegate bool (Widget id) { return (id !is null); };
712 lay.firstChild = delegate Widget (Widget id) { return id.firstChild; };
713 lay.nextSibling = delegate Widget (Widget id) { return id.nextSibling; };
715 lay.getMinSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.minSize.w : id.minSize.h); };
716 lay.getMaxSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.maxSize.w : id.maxSize.h); };
717 lay.getPrefSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.prefSize.w : id.prefSize.h); };
719 lay.isHorizBox = delegate bool (Widget id) { return (id.childDir == GxDir.Horiz); };
721 lay.getFlex = delegate int (Widget id) { return id.flex; };
723 lay.getSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.boxsize.w : id.boxsize.h); };
724 lay.setSize = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.boxsize.w = val; else id.boxsize.h = val; };
726 lay.getFinalSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.finalSize.w : id.finalSize.h); };
727 lay.setFinalSize = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.finalSize.w = val; else id.finalSize.h = val; };
729 lay.setFinalPos = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.finalPos.x = val; else id.finalPos.y = val; };
731 if (winrect.size.empty) {
732 if (maxWinSize.empty) maxWinSize = GxSize(screenWidth-decorationSizeX, screenHeight-decorationSizeY);
733 resizeWindow = true;
736 if (winrect.size.w > screenWidth) winrect.size.w = screenWidth;
737 if (winrect.size.h > screenHeight) winrect.size.h = screenHeight;
739 if (resizeWindow) {
740 mRoot.maxSize = maxWinSize;
741 } else {
742 mRoot.maxSize = winrect.size-GxSize(decorationSizeX, decorationSizeY);
743 if (mRoot.maxSize.w <= 0) mRoot.maxSize.w = maxWinSize.w;
744 if (mRoot.maxSize.h <= 0) mRoot.maxSize.h = maxWinSize.h;
747 mRoot.minSize = minWinSize;
748 if (mRoot.minSize.w > screenWidth-decorationSizeX) mRoot.minSize.w = screenWidth-decorationSizeX;
749 if (mRoot.minSize.h > screenHeight-decorationSizeY) mRoot.minSize.h = screenHeight-decorationSizeY;
751 if (!mNoTitle && mWinTitle.length) {
752 if (mRoot.boxsize.w <= 0) mRoot.boxsize.w = gxTextWidthUtf(mWinTitle)+2;
754 mRoot.prefSize = mRoot.boxsize;
756 Widget[] hsizes;
757 Widget[] vsizes;
758 scope(exit) {
759 hsizes.unsafeArrayClear();
760 delete hsizes;
761 vsizes.unsafeArrayClear();
762 delete vsizes;
765 foreach (Widget w; mRoot.allDepth) {
766 w.preLayout();
767 // hsize
768 w.hsizeIdNext = null;
769 if (w.hsizeId.length) {
770 foreach (ref Widget nw; hsizes) {
771 if (nw.hsizeId == w.hsizeId) {
772 w.hsizeIdNext = nw;
773 nw = w;
774 break;
777 if (w.hsizeIdNext is null) hsizes.unsafeArrayAppend(w);
779 // vsize
780 w.vsizeIdNext = null;
781 if (w.vsizeId.length) {
782 foreach (ref Widget nw; vsizes) {
783 if (nw.vsizeId == w.vsizeId) {
784 w.vsizeIdNext = nw;
785 nw = w;
786 break;
789 if (w.vsizeIdNext is null) vsizes.unsafeArrayAppend(w);
793 // fix horiz pref sizes
794 foreach (Widget sw; hsizes) {
795 int vmax = 0;
796 for (Widget w = sw; w !is null; w = w.hsizeIdNext) if (vmax < w.prefSize.w) vmax = w.prefSize.w;
797 for (Widget w = sw; w !is null; w = w.hsizeIdNext) w.prefSize.w = vmax;
800 // fix vert pref sizes
801 foreach (Widget sw; vsizes) {
802 int vmax = 0;
803 for (Widget w = sw; w !is null; w = w.vsizeIdNext) if (vmax < w.prefSize.h) vmax = w.prefSize.h;
804 for (Widget w = sw; w !is null; w = w.vsizeIdNext) w.prefSize.h = vmax;
807 lay.layout(mRoot);
809 winrect.size = mRoot.boxsize+GxSize(decorationSizeX, decorationSizeY);
810 if (winrect.x1 >= screenWidth) winrect.pos.x -= winrect.x1-screenWidth+1;
811 if (winrect.y1 >= screenHeight) winrect.pos.y -= winrect.y1-screenHeight+1;
812 if (winrect.pos.x < 0) winrect.pos.x = 0;
813 if (winrect.pos.y < 0) winrect.pos.y = 0;
815 foreach (Widget w; mRoot.allDepth) {
816 w.hsizeIdNext = null;
817 w.vsizeIdNext = null;
818 w.postLayout();
820 widgetChanged();
823 void relayoutResize () { relayout(true); }
825 final @property Widget rootWidget () pure nothrow @safe @nogc { pragma(inline, true); return mRoot; }
827 final @property int x0 () nothrow @safe { return (minimised ? winminx : winrect.pos.x); }
828 final @property int y0 () nothrow @safe { return (minimised ? winminy : winrect.pos.y); }
829 final @property int width () nothrow @safe { return (minimised ? MinSizeX : winrect.size.w); }
830 final @property int height () nothrow @safe { return (minimised ? MinSizeY : winrect.size.h); }
832 final @property void x0 (in int v) {
833 if (minimised) {
834 if (winminx != v) { winminx = v; if (mInWinList) widgetChanged(); }
835 } else {
836 if (winrect.pos.x != v) { winrect.pos.x = v; widgetChanged(); }
839 final @property void y0 (in int v) {
840 if (minimised) {
841 if (winminy != v) { winminy = v; if (mInWinList) widgetChanged(); }
842 } else {
843 if (winrect.pos.y != v) { winrect.pos.y = v; widgetChanged(); }
846 final @property void width (in int v) {
847 if (minimised) {
848 winrect.size.w = v;
849 } else {
850 if (winrect.size.w != v) { winrect.size.w = v; widgetChanged(); }
853 final @property void height (in int v) {
854 if (minimised) {
855 winrect.size.h = v;
856 } else {
857 if (winrect.size.h != v) { winrect.size.h = v; widgetChanged(); }
861 final void setPos (in int newx, in int newy) {
862 if (winrect.pos.x != newx || winrect.pos.y != newy) {
863 winrect.pos.x = newx;
864 winrect.pos.y = newy;
865 widgetChanged();
869 final void setSize (in int awidth, in int aheight) {
870 bool changed = false;
871 if (awidth > 0 && awidth != winrect.size.w) { changed = true; winrect.size.w = awidth; }
872 if (aheight > 0 && aheight != winrect.size.w) { changed = true; winrect.size.h = aheight; }
873 if (changed) {
874 fixRoot();
875 widgetChanged();
879 final void setClientSize (int awidth, int aheight) {
880 bool changed = false;
881 if (awidth > 0 && awidth+decorationSizeX != winrect.size.w) { changed = true; winrect.size.w = awidth+decorationSizeX; }
882 if (aheight > 0 && aheight+decorationSizeY != winrect.size.h) { changed = true; winrect.size.h = aheight+decorationSizeY; }
883 if (changed) {
884 fixRoot();
885 widgetChanged();
889 final void centerWindow () {
890 immutable int newx = (screenWidth-winrect.size.w)/2;
891 immutable int newy = (screenHeight-winrect.size.h)/2;
892 if (winrect.pos.x != newx || winrect.pos.y != newy) {
893 winrect.pos.x = newx;
894 winrect.pos.y = newy;
895 widgetChanged();
899 final @property SubWindow prev () pure nothrow @safe @nogc { return mPrev; }
900 final @property SubWindow next () pure nothrow @safe @nogc { return mNext; }
902 final @property Type type () const pure nothrow @safe @nogc { return mType; }
903 final @property bool onTop () const pure nothrow @safe @nogc { return (mType == Type.OnTop); }
904 final @property bool onBottom () const pure nothrow @safe @nogc { return (mType == Type.OnBottom); }
906 final @property bool inWinList () const pure nothrow @safe @nogc { return mInWinList; }
908 final @property bool modal () const pure nothrow @safe @nogc { return mModal; }
909 final @property bool closed () const pure nothrow @safe @nogc { return mClosed; }
911 final @property bool inWindowList () const pure nothrow @safe @nogc { return mInWinList; }
913 final @property bool active () const nothrow @trusted @nogc {
914 if (!mInWinList || mClosed || minimised) return false;
915 return (getActiveSubWindow is this);
918 @property int decorationSizeX () const nothrow @safe { return 2*2; }
919 @property int decorationSizeY () const nothrow @safe { immutable hgt = gxTextHeightUtf; return (hgt < 10 ? 10 : hgt+1)+4; }
921 @property int clientOffsetX () const nothrow @safe { return 2; }
922 @property int clientOffsetY () const nothrow @safe { immutable hgt = gxTextHeightUtf; return (hgt < 10 ? 10 : hgt+1)+2; }
924 final @property int clientWidth () const nothrow @safe { return winrect.size.w-decorationSizeX; }
925 final @property int clientHeight () const nothrow @safe { return winrect.size.h-decorationSizeY; }
927 protected void drawWidgets () {
928 setupClientClip();
929 if (mRoot !is null) mRoot.onPaint();
932 // draw window frame and background in "normal" state
933 protected void drawNormalDecoration () {
934 //immutable string act = (active ? null : "inactive");
935 gxDrawWindow(winrect, (mNoTitle ? null : mWinTitle.length ? mWinTitle.getData : ""),
936 getColor("frame"),
937 getColor("title-text"),
938 getColor("title-back"),
939 getColor("back"),
940 getColor("shadow-color"),
941 getInt("shadow-size", 0),
942 (getInt("shadow-dash", 0) > 0));
945 // draw window frame and background in "minimised" state
946 protected void drawWindowMinimised () {
947 //immutable string act = (active ? null : "inactive");
948 gxFillRect(winminx, winminy, MinSizeX, MinSizeY, getColor("back"));
949 gxDrawRect(winminx, winminy, MinSizeX, MinSizeY, getColor("frame"));
952 // this changes clip
953 protected void drawDecoration () {
954 if (minimised) {
955 if (gxClipRect.intersect(GxRect(winminx, winminy, MinSizeX, MinSizeY))) drawWindowMinimised();
956 } else {
957 setupClip();
958 drawNormalDecoration();
962 protected void drawDragRect () {
963 immutable uint clr = getColor("drag-overlay-back");
964 if (gxIsTransparent(clr)) return;
965 immutable bool dashed = (getInt("drag-overlay-dash", 0) > 0);
966 gxWithSavedClip {
967 gxClipReset();
968 if (dashed) {
969 gxDashRect(x0, y0, width, height, clr);
970 } else {
971 gxFillRect(x0, y0, width, height, clr);
977 void releaseWidgetGrab () {
978 if (mRoot !is null) mRoot.releaseGrab();
981 // event in our local coords
982 bool startMouseDrag (MouseEvent event) {
983 if (!mAllowDragMove || !mInWinList) return false;
984 releaseWidgetGrab();
985 subwinDrag = this;
986 subwinDragXSpot = -event.x;
987 subwinDragYSpot = -event.y;
988 widgetChanged();
989 return true;
992 bool startKeyboardDrag () {
993 if (!mAllowDragMove || !mInWinList) return false;
994 releaseWidgetGrab();
995 subwinDrag = this;
996 subwinDragXSpot = int.min;
997 subwinDragYSpot = int.min;
998 widgetChanged();
999 return true;
1002 void stopDrag () {
1003 if (subwinDrag is this) {
1004 releaseWidgetGrab();
1005 subwinDrag = null;
1006 widgetChanged();
1010 void onPaint () {
1011 if (closed) return;
1012 gxWithSavedClip {
1013 drawDecoration();
1014 if (!minimised) drawWidgets();
1019 bool onKeySink (KeyEvent event) {
1020 return false;
1023 bool onKeyBubble (KeyEvent event) {
1024 // global window hotkeys
1025 if (event.pressed) {
1026 if (event == "C-F5") { if (startKeyboardDrag()) return true; }
1027 if (/*event == "M-M" ||*/ event == "M-S-M") { if (minimise()) return true; }
1029 return false;
1032 bool onKeyEvent (KeyEvent event) {
1033 if (closed) return false;
1034 if (minimised) return false;
1035 if (onKeySink(event)) return true;
1036 if (mRoot !is null && mRoot.dispatchKey(event)) return true;
1037 return onKeyBubble(event);
1041 bool onMouseSink (MouseEvent event) {
1042 // start drag?
1043 if (subwinDrag is null && event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1044 if (event.x >= 0 && event.y >= 0 &&
1045 event.x < width && event.y < (!minimised ? gxTextHeightUtf+2 : height))
1047 startMouseDrag(event);
1048 return true;
1052 if (minimised) return false;
1054 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.right) {
1055 if (event.x >= 0 && event.y >= 0 &&
1056 event.x < winrect.size.w && event.y < gxTextHeightUtf)
1058 minimise();
1059 return true;
1063 return false;
1066 bool onMouseBubble (MouseEvent event) {
1067 return false;
1070 bool onMouseEvent (MouseEvent event) {
1071 if (!active) return false;
1072 if (closed) return false;
1074 int mx, my;
1075 event.mouse2xy(mx, my);
1077 MouseEvent ev = event;
1078 ev.x = mx-x0;
1079 ev.y = my-y0;
1080 if (onMouseSink(ev)) return true;
1082 if (mRoot !is null) {
1083 ev = event;
1084 ev.x = mx-(x0+clientOffsetX)-mRoot.rect.x0;
1085 ev.y = my-(y0+clientOffsetY)-mRoot.rect.y0;
1086 if (mRoot.dispatchMouse(ev)) return true;
1089 ev = event;
1090 ev.x = mx-x0;
1091 ev.y = my-y0;
1092 return onMouseBubble(ev);
1096 bool onCharSink (dchar ch) {
1097 return false;
1100 bool onCharBubble (dchar ch) {
1101 return false;
1104 bool onCharEvent (dchar ch) {
1105 if (!active) return false;
1106 if (closed) return false;
1107 if (minimised) return false;
1108 if (onCharSink(ch)) return true;
1109 if (mRoot !is null && mRoot.dispatchChar(ch)) return true;
1110 return onCharBubble(ch);
1113 void setupClip () {
1114 gxClipRect.intersect(winrect);
1117 final void setupClientClip () {
1118 setupClip();
1119 gxClipRect.intersect(GxRect(
1120 GxPoint(winrect.pos.x+clientOffsetX, winrect.pos.y+clientOffsetY),
1121 GxPoint(winrect.pos.x+clientOffsetX+clientWidth-1, winrect.pos.y+clientOffsetY+clientHeight-1)));
1124 void close () {
1125 mClosed = true;
1126 if (removeSubWindow(this)) widgetChanged();
1129 protected bool addToSubwinList (bool asModal, bool fromKeyboard) {
1130 if (fromKeyboard) ignoreSubWinChar = true;
1131 if (mInWinList) return true;
1132 mModal = asModal;
1133 if (insertSubWindow(this)) {
1134 widgetChanged();
1135 return true;
1137 return false;
1140 void add (bool fromKeyboard=false) { addToSubwinList(false, fromKeyboard); }
1142 void addModal (bool fromKeyboard=false) { addToSubwinList(true, fromKeyboard); }
1144 // return `false` to reject
1145 protected bool minimiseQuery () {
1146 return true;
1149 // return `false` to reject (this is "unminimise")
1150 protected bool restoreQuery () {
1151 return true;
1154 void bringToFront () {
1155 if (mClosed || !mInWinList) return;
1156 if (minimised && !restoreQuery()) return;
1157 auto aw = getActiveSubWindow();
1158 if (aw is this) {
1159 if (minimised) {
1160 widgetChanged();
1161 mMinimised = false;
1162 lastHoverWindow = null;
1164 return;
1166 if (aw !is null && aw.mModal) return; // alas
1167 removeSubWindow(this);
1168 mMinimised = false;
1169 insertSubWindow(this);
1170 if (subwinDrag !is this) subwinDrag = null;
1171 widgetChanged();
1174 final @property bool allowMinimise () pure const nothrow @safe @nogc { return mAllowMinimise; }
1175 @property void allowMinimise (in bool v) { mAllowMinimise = v; }
1177 final @property bool allowDragMove () pure const nothrow @safe @nogc { return mAllowDragMove; }
1178 @property void allowDragMove (in bool v) { mAllowDragMove = v; }
1180 final @property bool minimised () pure const nothrow @safe @nogc { return mMinimised; }
1181 @property void minimised (in bool v) {
1182 if (v == mMinimised) return;
1183 if (!mAllowMinimise && v) return;
1184 if (v) minimise(); else bringToFront();
1187 bool minimise () {
1188 if (minimised) return true;
1189 if (mClosed || !mAllowMinimise || mModal) return false;
1190 if (!mInWinList) {
1191 if (minimiseQuery()) mMinimised = true;
1192 return true;
1194 assert(subwinLast !is null);
1195 releaseWidgetGrab();
1196 findMinimisedPos(winminx, winminy);
1197 auto aw = getActiveSubWindow();
1198 if (aw is this) subwinDrag = null;
1199 mMinimised = true;
1200 lastHoverWindow = null;
1201 widgetChanged();
1202 return true;
1205 void restore () {
1206 releaseWidgetGrab();
1207 bringToFront();
1210 public:
1212 enum MinSizeX = 16;
1213 enum MinSizeY = 16;
1214 enum MinMarginX = 3;
1215 enum MinMarginY = 3;
1218 static T clampval(T) (in T val, in T min, in T max) pure nothrow @safe @nogc {
1219 pragma(inline, true);
1220 return (val < min ? min : val > max ? max : val);
1223 int MinSizeX () nothrow @safe { immutable bool omm = mMinimised; mMinimised = true; scope(exit) mMinimised = omm; return clampval(getInt("icon-size-x", 16), 16, 64); }
1224 int MinSizeY () nothrow @safe { immutable bool omm = mMinimised; mMinimised = true; scope(exit) mMinimised = omm; return clampval(getInt("icon-size-y", 16), 16, 64); }
1226 int MinMarginX () nothrow @safe { immutable bool omm = mMinimised; mMinimised = true; scope(exit) mMinimised = omm; return clampval(getInt("icon-margin-x", 3), 1, 16); }
1227 int MinMarginY () nothrow @safe { immutable bool omm = mMinimised; mMinimised = true; scope(exit) mMinimised = omm; return clampval(getInt("icon-margin-y", 3), 1, 16); }
1229 protected:
1230 void findMinimisedPos (out int wx, out int wy) {
1231 static bool isOccupied (int x, int y) {
1232 for (SubWindow w = subwinLast; w !is null; w = w.mPrev) {
1233 if (w.mInWinList && !w.closed && w.minimised) {
1234 if (x >= w.winminx && y >= w.winminy && x < w.winminx+w.MinSizeX && y < w.winminy+w.MinSizeY) return true;
1237 return false;
1240 int txcount = screenWidth/(MinSizeX+MinMarginX);
1241 //int tycount = screenHeight/(MinSizeY+MinMarginY);
1242 if (txcount < 1) txcount = 1;
1243 //if (tycount < 1) tycount = 1;
1244 foreach (immutable int n; 0..6/*5535*/) {
1245 int x = (n%txcount)*(MinSizeX+MinMarginX)+1;
1246 int y = screenHeight-MinSizeY-(n/txcount)*(MinSizeY+MinMarginY);
1247 //conwriteln("trying (", x, ",", y, ")");
1248 if (!isOccupied(x, y)) { wx = x; wy = y; /*conwriteln(" HIT!");*/ return; }