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 }