1 // Written in the D programming language.
2 
3 /**
4 
5 This module implements dockable windows UI support.
6 
7 DockHost is main layout for docking support - contains body widget and optional docked windows.
8 
9 DockWindow is window to use with DockHost - can be docked on top, left, right or bottom side of DockHost.
10 
11 Synopsis:
12 
13 ----
14 import dlangui.widgets.docks;
15 ----
16 
17 
18 Copyright: Vadim Lopatin, 2015
19 License:   Boost License 1.0
20 Authors:   Vadim Lopatin, coolreader.org@gmail.com
21 */
22 module dlangui.widgets.docks;
23 
24 import dlangui.widgets.layouts;
25 import dlangui.widgets.controls;
26 import dlangui.widgets.winframe;
27 
28 /// dock alignment types
29 enum DockAlignment {
30     /// at left of body
31     Left,
32     /// at right of body
33     Right,
34     /// above body
35     Top,
36     /// below body
37     Bottom
38 }
39 
40 
41 struct DockSpace {
42     protected Rect _rc;
43     protected Rect _resizerRect;
44     protected Rect _dockRect;
45     protected DockWindow[] _docks;
46     @property DockWindow[] docks() { return _docks; }
47     protected DockHost _host;
48     protected DockAlignment _alignment;
49     @property DockAlignment alignment() { return _alignment; }
50     protected ResizerWidget _resizer;
51     @property ResizerWidget resizer() { return _resizer; }
52     protected int _space;
53     @property int space() { return _space; }
54     protected int _minSpace;
55     protected int _maxSpace;
56     ResizerWidget initialize(DockHost host, DockAlignment a) {
57         _host = host;
58         _alignment = a;
59         final switch (a) with(DockAlignment)
60         {
61             case Top:
62                 _resizer =  new ResizerWidget("top_resizer", Orientation.Vertical);
63                 break;
64             case Bottom:
65                 _resizer =  new ResizerWidget("bottom_resizer", Orientation.Vertical);
66                 break;
67             case Left:
68                 _resizer =  new ResizerWidget("left_resizer", Orientation.Horizontal);
69                 break;
70             case Right:
71                 _resizer =  new ResizerWidget("right_resizer", Orientation.Horizontal);
72                 break;
73         }
74         _resizer.visibility = Visibility.Gone;
75         _resizer.resizeEvent = &onResize;
76         return _resizer;
77     }
78     /// host to be layed out
79     void beforeLayout(Rect rc, DockWindow[] docks) {
80         _docks = docks;
81         int baseSize;
82         if (_resizer.orientation == Orientation.Vertical)
83             baseSize = rc.height;
84         else
85             baseSize = rc.width;
86         _minSpace = baseSize * 1 / 10;
87         _maxSpace = baseSize * 4 / 10;
88         if (_docks.length) {
89             if (_space < _minSpace) {
90                 if (_space == 0)
91                     _space = _minSpace + (_maxSpace - _minSpace) * 1 / 3;
92                 else
93                     _space = _minSpace;
94             }
95             if (_space > _maxSpace) {
96                 _space = _maxSpace;
97             }
98             _resizer.visibility = Visibility.Visible;
99         } else {
100             _space = 0;
101             _resizer.visibility = Visibility.Gone;
102         }
103     }
104     void layout(Rect rc) {
105         int rsWidth = 3; // resizer width
106         if (_space) {
107             _rc = rc;
108             final switch (_alignment) with(DockAlignment)
109             {
110                 case Top:
111                     _resizerRect = Rect(rc.left, rc.bottom - rsWidth, rc.right, rc.bottom + rsWidth);
112                     _dockRect = Rect(rc.left, rc.top, rc.right, rc.bottom - rsWidth);
113                     break;
114                 case Bottom:
115                     _resizerRect = Rect(rc.left, rc.top - rsWidth, rc.right, rc.top + rsWidth);
116                     _dockRect = Rect(rc.left, rc.top + rsWidth, rc.right, rc.bottom);
117                     break;
118                 case Left:
119                     _resizerRect = Rect(rc.right - rsWidth, rc.top, rc.right + rsWidth, rc.bottom);
120                     _dockRect = Rect(rc.left, rc.top, rc.right - rsWidth, rc.bottom);
121                     break;
122                 case Right:
123                     _resizerRect = Rect(rc.left - rsWidth, rc.top, rc.left + rsWidth, rc.bottom);
124                     _dockRect = Rect(rc.left + rsWidth, rc.top, rc.right, rc.bottom);
125                     break;
126             }
127             // layout resizer
128             _resizer.layout(_resizerRect);
129             // layout docked
130             layoutDocked();
131         } else {
132             _rc = _resizerRect = _dockRect = Rect(0, 0, 0, 0); // empty rect
133         }
134     }
135     protected int _dragStartSpace;
136     protected int _dragStartPosition;
137     protected void onResize(ResizerWidget source, ResizerEventType event, int newPosition) {
138         if (!_space)
139             return;
140         if (event == ResizerEventType.StartDragging) {
141             _dragStartSpace = _space;
142             _dragStartPosition = newPosition;
143         } else if (event == ResizerEventType.Dragging) {
144             int dir = _alignment == DockAlignment.Right || _alignment == DockAlignment.Bottom ? -1 : 1;
145             _space = _dragStartSpace + dir * (newPosition - _dragStartPosition);
146             _host.onResize(source, event, newPosition);
147         }
148     }
149     protected void layoutDocked() {
150         Rect rc = _rc; //_dockRect;
151         int len = cast(int)_docks.length;
152         for (int i = 0; i < len; i++) {
153             Rect itemRc = rc;
154             if (len > 1) {
155                 if (_resizer.orientation == Orientation.Horizontal) {
156                     itemRc.top = rc.top + rc.height * i / len;
157                     if (i != len - 1)
158                         itemRc.bottom = rc.top + rc.height * (i + 1) / len;
159                     else
160                         itemRc.bottom = rc.bottom;
161                 } else {
162                     itemRc.left = rc.left + rc.width * i / len;
163                     if (i != len - 1)
164                         itemRc.right = rc.left + rc.width * (i + 1) / len;
165                     else
166                         itemRc.right = rc.right;
167                 }
168             }
169             _docks[i].layout(itemRc);
170         }
171     }
172 }
173 
174 /// Layout for docking support - contains body widget and optional docked windows
175 class DockHost : WidgetGroupDefaultDrawing {
176 
177     
178     protected DockSpace _topSpace;
179     protected DockSpace _bottomSpace;
180     protected DockSpace _rightSpace;
181     protected DockSpace _leftSpace;
182     protected Widget _bodyWidget;
183     @property Widget bodyWidget() { return _bodyWidget; }
184     @property void bodyWidget(Widget widget) { 
185         _children.replace(widget, _bodyWidget);
186         _bodyWidget = widget;
187         _bodyWidget.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
188         _bodyWidget.parent = this;
189     }
190 
191     void addDockedWindow(DockWindow dockWin) {
192         addChild(dockWin);
193     }
194 
195     DockWindow removeDockedWindow(string id) {
196         DockWindow res = childById!DockWindow(id);
197         if (res)
198             removeChild(id);
199         return res;
200     }
201 
202     protected int _resizeStartPos;
203     void onResize(ResizerWidget source, ResizerEventType event, int newPosition) {
204         layout(_pos);
205     }
206 
207     this() {
208         super("DOCK_HOST");
209         styleId = STYLE_DOCK_HOST;
210         addChild(_topSpace.initialize(this, DockAlignment.Top));
211         addChild(_bottomSpace.initialize(this, DockAlignment.Bottom));
212         addChild(_leftSpace.initialize(this, DockAlignment.Left));
213         addChild(_rightSpace.initialize(this, DockAlignment.Right));
214     }
215 
216     protected DockWindow[] getDockedWindowList(DockAlignment alignType) {
217         DockWindow[] list;
218         for (int i = 0; i < _children.count; i++) {
219             DockWindow item = cast(DockWindow)_children.get(i);
220             if (!item)
221                 continue; // not a docked window
222             if(item.dockAlignment == alignType && item.visibility == Visibility.Visible) {
223                 list ~= item;
224             }
225         }
226         return list;
227     }
228 
229     protected DockAlignment[4] _layoutPriority = [DockAlignment.Top, DockAlignment.Left, DockAlignment.Right, DockAlignment.Bottom];
230     @property DockAlignment[4] layoutPriority() { return _layoutPriority; }
231     @property void layoutPriority(DockAlignment[4] p) { 
232         _layoutPriority = p;
233         requestLayout();
234     }
235 
236     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
237     override void layout(Rect rc) {
238         _needLayout = false;
239         if (visibility == Visibility.Gone) {
240             return;
241         }
242         _pos = rc;
243         applyMargins(rc);
244         applyPadding(rc);
245         foreach(a; _layoutPriority) {
246             if (a == DockAlignment.Top) _topSpace.beforeLayout(rc, getDockedWindowList(DockAlignment.Top));
247             if (a == DockAlignment.Left) _leftSpace.beforeLayout(rc, getDockedWindowList(DockAlignment.Left));
248             if (a == DockAlignment.Right) _rightSpace.beforeLayout(rc, getDockedWindowList(DockAlignment.Right));
249             if (a == DockAlignment.Bottom) _bottomSpace.beforeLayout(rc, getDockedWindowList(DockAlignment.Bottom));
250         }
251         int topsp, bottomsp, leftsp, rightsp;
252         foreach(a; _layoutPriority) {
253             if (a == DockAlignment.Top) {
254                 _topSpace.layout(Rect(rc.left + leftsp, rc.top, rc.right - rightsp, rc.top + _topSpace.space));
255                 topsp = _topSpace.space;
256             }
257             if (a == DockAlignment.Bottom) {
258                 _bottomSpace.layout(Rect(rc.left + leftsp, rc.bottom - _bottomSpace.space, rc.right - rightsp, rc.bottom));
259                 bottomsp = _bottomSpace.space;
260             }
261             if (a == DockAlignment.Left) {
262                 _leftSpace.layout(Rect(rc.left, rc.top + topsp, rc.left + _leftSpace.space, rc.bottom - bottomsp));
263                 leftsp = _leftSpace.space;
264             }
265             if (a == DockAlignment.Right) {
266                 _rightSpace.layout(Rect(rc.right - _rightSpace.space, rc.top + topsp, rc.right, rc.bottom - bottomsp));
267                 rightsp = _rightSpace.space;
268             }
269         }
270         if (_bodyWidget)
271             _bodyWidget.layout(Rect(rc.left + _leftSpace.space, rc.top + _topSpace.space, rc.right - _rightSpace.space, rc.bottom - _bottomSpace.space));
272     }
273 
274     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
275     override void measure(int parentWidth, int parentHeight) { 
276         Rect m = margins;
277         Rect p = padding;
278         // calc size constraints for children
279         int pwidth = parentWidth;
280         int pheight = parentHeight;
281         if (parentWidth != SIZE_UNSPECIFIED)
282             pwidth -= m.left + m.right + p.left + p.right;
283         if (parentHeight != SIZE_UNSPECIFIED)
284             pheight -= m.top + m.bottom + p.top + p.bottom;
285         // measure children
286         Point sz;
287         Point bodySize;
288         if (_bodyWidget) {
289             _bodyWidget.measure(pwidth, pheight);
290             bodySize.x = _bodyWidget.measuredWidth;
291             bodySize.y = _bodyWidget.measuredHeight;
292         }
293         for (int i = 0; i < _children.count; i++) {
294             Widget item = _children.get(i);
295             // TODO: fix
296             if (item.visibility != Visibility.Gone) {
297                 item.measure(pwidth, pheight);
298                 if (sz.x < item.measuredWidth)
299                     sz.x = item.measuredWidth;
300                 if (sz.y < item.measuredHeight)
301                     sz.y = item.measuredHeight;
302             }
303         }
304         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
305     }
306 }
307 
308 /// docked window
309 class DockWindow : WindowFrame {
310 
311     protected DockAlignment _dockAlignment;
312 
313     @property DockAlignment dockAlignment() { return _dockAlignment; }
314     @property DockWindow dockAlignment(DockAlignment a) { 
315         if (_dockAlignment != a) {
316             _dockAlignment = a;
317             requestLayout();
318         }
319         return this; 
320     }
321 
322     this(string ID) {
323         super(ID);
324         focusGroup = true;
325     }
326 
327     override protected void initialize() {
328         super.initialize();
329         _dockAlignment = DockAlignment.Right; // default alignment is right
330     }
331 
332     //protected Widget createBodyWidget() {
333     //    return new Widget("DOCK_WINDOW_BODY");
334     //}
335 }