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         this("DOCK_HOST");
209     }
210 
211     this(string ID) {
212         super(ID);
213         styleId = STYLE_DOCK_HOST;
214         addChild(_topSpace.initialize(this, DockAlignment.Top));
215         addChild(_bottomSpace.initialize(this, DockAlignment.Bottom));
216         addChild(_leftSpace.initialize(this, DockAlignment.Left));
217         addChild(_rightSpace.initialize(this, DockAlignment.Right));
218     }
219 
220     protected DockWindow[] getDockedWindowList(DockAlignment alignType) {
221         DockWindow[] list;
222         for (int i = 0; i < _children.count; i++) {
223             DockWindow item = cast(DockWindow)_children.get(i);
224             if (!item)
225                 continue; // not a docked window
226             if(item.dockAlignment == alignType && item.visibility == Visibility.Visible) {
227                 list ~= item;
228             }
229         }
230         return list;
231     }
232 
233     protected DockAlignment[4] _layoutPriority = [DockAlignment.Top, DockAlignment.Left, DockAlignment.Right, DockAlignment.Bottom];
234     @property DockAlignment[4] layoutPriority() { return _layoutPriority; }
235     @property void layoutPriority(DockAlignment[4] p) {
236         _layoutPriority = p;
237         requestLayout();
238     }
239 
240     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
241     override void layout(Rect rc) {
242         _needLayout = false;
243         if (visibility == Visibility.Gone) {
244             return;
245         }
246         _pos = rc;
247         applyMargins(rc);
248         applyPadding(rc);
249         foreach(a; _layoutPriority) {
250             if (a == DockAlignment.Top) _topSpace.beforeLayout(rc, getDockedWindowList(DockAlignment.Top));
251             if (a == DockAlignment.Left) _leftSpace.beforeLayout(rc, getDockedWindowList(DockAlignment.Left));
252             if (a == DockAlignment.Right) _rightSpace.beforeLayout(rc, getDockedWindowList(DockAlignment.Right));
253             if (a == DockAlignment.Bottom) _bottomSpace.beforeLayout(rc, getDockedWindowList(DockAlignment.Bottom));
254         }
255         int topsp, bottomsp, leftsp, rightsp;
256         foreach(a; _layoutPriority) {
257             if (a == DockAlignment.Top) {
258                 _topSpace.layout(Rect(rc.left + leftsp, rc.top, rc.right - rightsp, rc.top + _topSpace.space));
259                 topsp = _topSpace.space;
260             }
261             if (a == DockAlignment.Bottom) {
262                 _bottomSpace.layout(Rect(rc.left + leftsp, rc.bottom - _bottomSpace.space, rc.right - rightsp, rc.bottom));
263                 bottomsp = _bottomSpace.space;
264             }
265             if (a == DockAlignment.Left) {
266                 _leftSpace.layout(Rect(rc.left, rc.top + topsp, rc.left + _leftSpace.space, rc.bottom - bottomsp));
267                 leftsp = _leftSpace.space;
268             }
269             if (a == DockAlignment.Right) {
270                 _rightSpace.layout(Rect(rc.right - _rightSpace.space, rc.top + topsp, rc.right, rc.bottom - bottomsp));
271                 rightsp = _rightSpace.space;
272             }
273         }
274         if (_bodyWidget)
275             _bodyWidget.layout(Rect(rc.left + _leftSpace.space, rc.top + _topSpace.space, rc.right - _rightSpace.space, rc.bottom - _bottomSpace.space));
276     }
277 
278     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
279     override void measure(int parentWidth, int parentHeight) {
280         Rect m = margins;
281         Rect p = padding;
282         // calc size constraints for children
283         int pwidth = parentWidth;
284         int pheight = parentHeight;
285         if (parentWidth != SIZE_UNSPECIFIED)
286             pwidth -= m.left + m.right + p.left + p.right;
287         if (parentHeight != SIZE_UNSPECIFIED)
288             pheight -= m.top + m.bottom + p.top + p.bottom;
289         // measure children
290         Point sz;
291         Point bodySize;
292         if (_bodyWidget) {
293             _bodyWidget.measure(pwidth, pheight);
294             bodySize.x = _bodyWidget.measuredWidth;
295             bodySize.y = _bodyWidget.measuredHeight;
296         }
297         for (int i = 0; i < _children.count; i++) {
298             Widget item = _children.get(i);
299             // TODO: fix
300             if (item.visibility != Visibility.Gone) {
301                 item.measure(pwidth, pheight);
302                 if (sz.x < item.measuredWidth)
303                     sz.x = item.measuredWidth;
304                 if (sz.y < item.measuredHeight)
305                     sz.y = item.measuredHeight;
306             }
307         }
308         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
309     }
310 }
311 
312 /// docked window
313 class DockWindow : WindowFrame {
314 
315     protected DockAlignment _dockAlignment;
316 
317     @property DockAlignment dockAlignment() { return _dockAlignment; }
318     @property DockWindow dockAlignment(DockAlignment a) {
319         if (_dockAlignment != a) {
320             _dockAlignment = a;
321             requestLayout();
322         }
323         return this;
324     }
325 
326     this(string ID) {
327         super(ID);
328         focusGroup = true;
329     }
330 
331     override protected void initialize() {
332         super.initialize();
333         _dockAlignment = DockAlignment.Right; // default alignment is right
334     }
335 
336     //protected Widget createBodyWidget() {
337     //    return new Widget("DOCK_WINDOW_BODY");
338     //}
339 }