1 /**
2 * ae.ui.wm.controls.control
3 *
4 * License:
5 * This Source Code Form is subject to the terms of
6 * the Mozilla Public License, v. 2.0. If a copy of
7 * the MPL was not distributed with this file, You
8 * can obtain one at http://mozilla.org/MPL/2.0/.
9 *
10 * Authors:
11 * Vladimir Panteleev <ae@cy.md>
12 */
13
14 module ae.ui.wm.controls.control;
15
16 import std.algorithm : max;
17
18 import ae.ui.shell.events;
19 import ae.ui.video.renderer;
20
21 /// Root control class.
22 class Control
23 {
24 /// Geometry.
25 int x, y, w, h;
26
27 /// Event handlers.
28 void handleMouseDown(int x, int y, MouseButton button) {}
29 void handleMouseUp(int x, int y, MouseButton button) {} /// ditto
30 void handleMouseMove(int x, int y, MouseButtons buttons) {} /// ditto
31
32 /// Renderer.
33 abstract void render(Renderer r, int x, int y);
34
35 /// Parent getter.
36 final @property ContainerControl parent()
37 {
38 return _parent;
39 }
40
41 /// Parent setter.
42 final @property void parent(ContainerControl newParent)
43 {
44 if (_parent)
45 _parent._removeChild(this);
46 _parent = newParent;
47 _parent._addChild(this);
48 }
49
50 /// rw and rh are recommended (hint) sizes that the parent is allocating to the child,
51 /// but there is no obligation to follow them.
52 protected void arrange(int rw, int rh) { }
53
54 /// Called when a child's dimensions change, to allow the size change to bubble up to parents.
55 final void rearrange()
56 {
57 auto oldW = w, oldH = h;
58 arrange(w, h);
59 if (parent && (w != oldW || h != oldH))
60 parent.rearrange();
61 }
62
63 private:
64 ContainerControl _parent;
65 }
66
67 // ***************************************************************************
68
69 /// An abstract base class for a control with children.
70 class ContainerControl : Control
71 {
72 /// Find the child control at the given coordinates.
73 final Control controlAt(int x, int y)
74 {
75 foreach (child; children)
76 if (x>=child.x && x<child.x+child.w && y>=child.y && y<child.y+child.h)
77 return child;
78 return null;
79 }
80
81 /// Propagates the event to the control at the given coordinates.
82 override void handleMouseDown(int x, int y, MouseButton button)
83 {
84 auto child = controlAt(x, y);
85 if (child)
86 child.handleMouseDown(x-child.x, y-child.y, button);
87 }
88
89 override void handleMouseUp(int x, int y, MouseButton button)
90 {
91 auto child = controlAt(x, y);
92 if (child)
93 child.handleMouseUp(x-child.x, y-child.y, button);
94 } /// ditto
95
96 override void handleMouseMove(int x, int y, MouseButtons buttons)
97 {
98 auto child = controlAt(x, y);
99 if (child)
100 child.handleMouseMove(x-child.x, y-child.y, buttons);
101 } /// ditto
102
103 /// Renders all children.
104 override void render(Renderer s, int x, int y)
105 {
106 // background should be rendered by a subclass or parent
107 foreach (child; children)
108 child.render(s, x+child.x, y+child.y);
109 }
110
111 /// Children getter.
112 final @property Control[] children()
113 {
114 return _children;
115 }
116
117 /// Makes this control the given control's parent.
118 final typeof(this) addChild(Control control)
119 {
120 control.parent = this;
121 return this;
122 }
123
124 private:
125 // An array should be fine, performance-wise.
126 // UI manipulations should be infrequent.
127 Control[] _children;
128
129 final void _addChild(Control target)
130 {
131 _children ~= target;
132 }
133
134 final void _removeChild(Control target)
135 {
136 foreach (i, child; _children)
137 if (child is target)
138 {
139 _children = _children[0..i] ~ _children[i+1..$];
140 return;
141 }
142 assert(false, "Attempting to remove inexisting child");
143 }
144 }
145
146 /// Container with static child positions.
147 /// Does not rearrange its children.
148 /// Dimensions are bound by the lowest/right-most child.
149 class StaticFitContainerControl : ContainerControl
150 {
151 override void arrange(int rw, int rh)
152 {
153 int maxX, maxY;
154 foreach (child; children)
155 {
156 maxX = max(maxX, child.x + child.w);
157 maxY = max(maxY, child.y + child.h);
158 }
159 w = maxX;
160 h = maxY;
161 } ///
162 }
163
164 // ***************************************************************************
165
166 /// Allow specifying a size as a combination of parent size % and pixels.
167 /// Sizes are summed together.
168 struct RelativeSize
169 {
170 int px; /// Flat size in pixels.
171 float ratio; /// Parent size ratio.
172 // TODO: Add "em", when we have variable font sizes?
173
174 /// Get total resulting size.
175 int toPixels(int parentSize) pure const { return px + cast(int)(parentSize*ratio); }
176
177 RelativeSize opBinary(string op)(RelativeSize other)
178 if (op == "+" || op == "-")
179 {
180 return mixin("RelativeSize(this.px"~op~"other.px, this.ratio"~op~"other.ratio)");
181 } ///
182 }
183
184 /// Usage: 50.px
185 @property RelativeSize px(int px) { return RelativeSize(px, 0); }
186 /// Usage: 25.percent
187 @property RelativeSize percent(float percent) { return RelativeSize(0, percent/100f); }
188
189 // ***************************************************************************
190
191 /// No-op wrapper
192 class Wrapper : ContainerControl
193 {
194 override void arrange(int rw, int rh)
195 {
196 assert(children.length == 1, "Wrapper does not have exactly one child");
197 auto child = children[0];
198 child.arrange(rw, rh);
199 this.w = child.w;
200 this.h = child.h;
201 } ///
202 }
203
204
205 /// Provides default implementations for wrapper behavior methods
206 mixin template ComplementWrapperBehavior(alias WrapperBehavior, Params...)
207 {
208 final:
209 mixin WrapperBehavior;
210
211 void _moreMagic() {}
212
213 /// The default implementations.
214 static if (!is(typeof(adjustHint)))
215 int adjustHint(int hint, Params params) { return hint; }
216 static if (!is(typeof(adjustSize)))
217 int adjustSize(int size, int hint, Params params) { return size; } /// ditto
218 static if (!is(typeof(adjustPos)))
219 int adjustPos(int pos, int size, int hint, Params params) { return pos; } /// ditto
220 }
221
222 private mixin template OneDirectionCustomWrapper(alias WrapperBehavior, Params...)
223 {
224 private Params params;
225 static if (Params.length)
226 this(Params params)
227 {
228 this.params = params;
229 }
230
231 /// Declares adjustHint, adjustSize, adjustPos
232 mixin ComplementWrapperBehavior!(WrapperBehavior, Params);
233 }
234
235 private class WCustomWrapper(alias WrapperBehavior, Params...) : Wrapper
236 {
237 override void arrange(int rw, int rh)
238 {
239 assert(children.length == 1, "Wrapper does not have exactly one child");
240 auto child = children[0];
241 child.arrange(adjustHint(rw, params), rh);
242 this.w = adjustSize(child.w, rw, params);
243 this.h = child.h;
244 child.x = adjustPos(child.x, child.w, rw, params);
245 } ///
246
247 mixin OneDirectionCustomWrapper!(WrapperBehavior, Params);
248 }
249
250 private class HCustomWrapper(alias WrapperBehavior, Params...) : Wrapper
251 {
252 override void arrange(int rw, int rh)
253 {
254 assert(children.length == 1, "Wrapper does not have exactly one child");
255 auto child = children[0];
256 child.arrange(rw, adjustHint(rh, params));
257 this.w = child.w;
258 this.h = adjustSize(child.h, rh, params);
259 child.y = adjustPos(child.y, child.h, rh, params);
260 }
261
262 mixin OneDirectionCustomWrapper!(WrapperBehavior, Params);
263 }
264
265 private class CustomWrapper(alias WrapperBehavior, Params...) : Wrapper
266 {
267 override void arrange(int rw, int rh)
268 {
269 assert(children.length == 1, "Wrapper does not have exactly one child");
270 auto child = children[0];
271 child.arrange(adjustHint(rw, paramsX), adjustHint(rh, paramsY));
272 this.w = adjustSize(child.w, rw, paramsX);
273 this.h = adjustSize(child.h, rh, paramsY);
274 child.x = adjustPos(child.x, child.w, rw, paramsX);
275 child.y = adjustPos(child.y, child.h, rh, paramsY);
276 }
277
278 private Params paramsX, paramsY;
279 static if (Params.length)
280 this(Params paramsX, Params paramsY)
281 {
282 this.paramsX = paramsX;
283 this.paramsY = paramsY;
284 }
285
286 /// Declares adjustHint, adjustSize, adjustPos
287 mixin ComplementWrapperBehavior!(WrapperBehavior, Params);
288 }
289
290 mixin template _DeclareWrapper(string name, alias WrapperBehavior, Params...)
291 {
292 mixin(`alias WCustomWrapper!(WrapperBehavior, Params) W`~name~`;`);
293 mixin(`alias HCustomWrapper!(WrapperBehavior, Params) H`~name~`;`);
294 mixin(`alias CustomWrapper!(WrapperBehavior, Params) `~name~`;`);
295 }
296
297 private mixin template SizeBehavior()
298 {
299 int adjustHint(int hint, RelativeSize size)
300 {
301 return size.toPixels(hint);
302 }
303 }
304 /// Wrapper to override the parent hint to a specific size.
305 mixin _DeclareWrapper!("Size", SizeBehavior, RelativeSize);
306
307 private mixin template ShrinkBehavior()
308 {
309 int adjustHint(int hint)
310 {
311 return 0;
312 }
313 }
314 /// Wrapper to override the parent hint to 0, thus making
315 /// the wrapped control as small as it can be.
316 mixin _DeclareWrapper!("Shrink", ShrinkBehavior);
317
318 private mixin template CenterBehavior()
319 {
320 int adjustSize(int size, int hint)
321 {
322 return max(size, hint);
323 }
324
325 int adjustPos(int pos, int size, int hint)
326 {
327 if (hint < size) hint = size;
328 return (hint-size)/2;
329 }
330 }
331 /// If content is smaller than parent hint, center the content and use parent hint for own size.
332 mixin _DeclareWrapper!("Center", CenterBehavior);
333
334 private mixin template PadBehavior()
335 {
336 int adjustHint(int hint, RelativeSize padding)
337 {
338 auto paddingPx = padding.toPixels(hint);
339 return max(0, hint - paddingPx*2);
340 }
341
342 int adjustSize(int size, int hint, RelativeSize padding)
343 {
344 auto paddingPx = padding.toPixels(hint);
345 return size + paddingPx*2;
346 }
347
348 int adjustPos(int pos, int size, int hint, RelativeSize padding)
349 {
350 auto paddingPx = padding.toPixels(hint);
351 return paddingPx;
352 }
353 }
354 /// Add some padding on both sides of the content.
355 mixin _DeclareWrapper!("Pad", PadBehavior, RelativeSize);
356
357 // ***************************************************************************
358
359 /// Space out controls in a 2D grid, according to their dimensions and resizability.
360 class Table : ContainerControl
361 {
362 /// Table size.
363 uint rows, cols;
364
365 this(uint rows, uint cols)
366 {
367 this.rows = rows;
368 this.cols = cols;
369 } ///
370
371 override void arrange(int rw, int rh)
372 {
373 assert(children.length == rows*cols, "Wrong number of table children");
374
375 static struct Size { int w, h; }
376 Size[][] minSizes = new Size[][](cols, rows);
377 int[] minColSizes = new int[cols];
378 int[] minRowSizes = new int[rows];
379
380 foreach (i, child; children)
381 {
382 child.arrange(0, 0);
383 auto col = i % cols;
384 auto row = i / cols;
385 minSizes[row][col] = Size(child.w, child.h);
386 minColSizes[col] = max(minColSizes[col], child.w);
387 minRowSizes[row] = max(minRowSizes[row], child.h);
388 }
389
390 import std.algorithm;
391 int minW = reduce!"a + b"(0, minColSizes);
392 int minH = reduce!"a + b"(0, minRowSizes);
393
394 // If all controls can take up no space, spread them out equivalently
395 if (minW == 0) { minW = cols; minColSizes[] = 1; }
396 if (minH == 0) { minH = rows; minRowSizes[] = 1; }
397
398 // TODO: fixed-size rows / columns
399 // Maybe associate RelativeSize values with rows/columns?
400
401 this.w = max(minW, rw);
402 this.h = max(minH, rh);
403
404 int[] colSizes = new int[cols];
405 int[] colOffsets = new int[cols];
406 int p = 0;
407 foreach (col; 0..cols)
408 {
409 colOffsets[col] = p;
410 auto size = minW ? minColSizes[col] * this.w / minW : 0;
411 colSizes[col] = size;
412 p += size;
413 }
414
415 int[] rowSizes = new int[rows];
416 int[] rowOffsets = new int[rows];
417 p = 0;
418 foreach (row; 0..rows)
419 {
420 rowOffsets[row] = p;
421 auto size = minH ? minRowSizes[row] * this.h / minH : 0;
422 rowSizes[row] = size;
423 p += size;
424 }
425
426 foreach (i, child; children)
427 {
428 auto col = i % cols;
429 auto row = i / cols;
430 child.x = colOffsets[col];
431 child.y = rowOffsets[row];
432 child.arrange(colSizes[col], rowSizes[col]);
433 }
434 } ///
435 }
436
437 /// 1D table for a row of controls.
438 class Row : Table
439 {
440 this() { super(0, 0); } ///
441
442 override void arrange(int rw, int rh)
443 {
444 rows = 1;
445 cols = cast(uint)children.length;
446 super.arrange(rw, rh);
447 } ///
448 }
449
450 /// 1D table for a column of controls.
451 class Column : Table
452 {
453 this() { super(0, 0); } ///
454
455 override void arrange(int rw, int rh)
456 {
457 rows = cast(uint)children.length;
458 cols = 1;
459 super.arrange(rw, rh);
460 } ///
461 }
462
463 // ***************************************************************************
464
465 /// All children occupy the entire area of the control.
466 /// The control grows as necessary to accommodate all layers.
467 class Layers : ContainerControl
468 {
469 override void arrange(int rw, int rh)
470 {
471 w = rw; h = rh;
472 bool changed;
473 do
474 {
475 changed = false;
476 foreach (child; children)
477 {
478 child.arrange(w, h);
479 if (child.w > w)
480 w = child.w, changed = true;
481 if (child.h > h)
482 h = child.h, changed = true;
483 }
484 } while (changed);
485 } ///
486 }
487
488 // ***************************************************************************
489
490 /// Container for all top-level windows.
491 /// The root control's children are, semantically, layers.
492 final class RootControl : ContainerControl
493 {
494 override void arrange(int rw, int rh)
495 {
496 foreach (child; children)
497 child.arrange(w, h);
498 } ///
499
500 // Expose "arrange", which is "protected", to `WMApplication`
501 /// Called by `WMApplication` to notify of size changes.
502 final void sizeChanged()
503 {
504 arrange(w, h);
505 }
506 }