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