1 /**
2 Grimoire
3 Copyright (c) 2017 Enalye
4 
5 This software is provided 'as-is', without any express or implied warranty.
6 In no event will the authors be held liable for any damages arising
7 from the use of this software.
8 
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute
11 it freely, subject to the following restrictions:
12 
13 	1. The origin of this software must not be misrepresented;
14 	   you must not claim that you wrote the original software.
15 	   If you use this software in a product, an acknowledgment
16 	   in the product documentation would be appreciated but
17 	   is not required.
18 
19 	2. Altered source versions must be plainly marked as such,
20 	   and must not be misrepresented as being the original software.
21 
22 	3. This notice may not be removed or altered from any source distribution.
23 */
24 
25 module ui.widget;
26 
27 import render.window;
28 import core.all;
29 import common.all;
30 
31 import ui.overlay;
32 import ui.modal;
33 
34 private {
35 	bool _isWidgetDebug = false;
36 }
37 
38 void setWidgetDebug(bool isDebug) {
39 	_isWidgetDebug = isDebug;
40 }
41 
42 interface IMainWidget {
43 	void onEvent(Event event);
44 }
45 
46 class Widget {
47 	protected {
48 		Hint _hint;
49 		bool _isLocked = false, _isMovable = false, _isHovered = false, _isSelected = false, _isValidated = false, _hasFocus = false, _isInteractable = true;
50 		Vec2f _position = Vec2f.zero, _size = Vec2f.zero, _anchor = Vec2f.half, _padding = Vec2f.zero;
51 		float _angle = 0f;
52 		Widget _callbackWidget;
53 		string _callbackId;
54 	}
55 
56 	@property {
57 		final bool isLocked() const { return _isLocked; }
58 		final bool isLocked(bool newIsLocked) {
59             if(newIsLocked != _isLocked) {
60                 _isLocked = newIsLocked;
61                 onLock();
62                 return _isLocked;
63             }
64             return _isLocked = newIsLocked;
65         }
66 
67 		final bool isMovable() const { return _isMovable; }
68 		final bool isMovable(bool newIsMovable) {
69             if(newIsMovable != _isMovable) {
70                 _isMovable = newIsMovable;
71                 onMovable();
72                 return _isMovable;
73             }
74             return _isMovable = newIsMovable;
75         }
76 
77 		final bool isHovered() const { return _isHovered; }
78 		final bool isHovered(bool newIsHovered) {
79             if(newIsHovered != _isHovered) {
80                 _isHovered = newIsHovered;
81                 onHover();
82                 return _isHovered;
83             }
84             return _isHovered = newIsHovered;
85         }
86 
87 		final bool isSelected() const { return _isSelected; }
88 		final bool isSelected(bool newIsSelected) {
89             if(newIsSelected != _isSelected) {
90                 _isSelected = newIsSelected;
91                 onSelect();
92                 return _isSelected;
93             }
94             return _isSelected = newIsSelected;
95         }
96 
97 		final bool hasFocus() const { return _hasFocus; }
98 		final bool hasFocus(bool newHasFocus) {
99             if(newHasFocus != _hasFocus) {
100                 _hasFocus = newHasFocus;
101                 onFocus();
102                 return _hasFocus;
103             }
104             return _hasFocus = newHasFocus;
105         }
106 
107 		final bool isInteractable() const { return _isInteractable; }
108 		final bool isInteractable(bool newIsInteractable) {
109             if(newIsInteractable != _isInteractable) {
110                 _isInteractable = newIsInteractable;
111                 onInteractable();
112                 return _isInteractable;
113             }
114             return _isInteractable = newIsInteractable;
115         }
116 
117 		final bool isValidated() const { return _isValidated; }
118 		final bool isValidated(bool newIsValidated) {
119             if(newIsValidated != _isValidated) {
120                 _isValidated = newIsValidated;
121                 onValidate();
122                 return _isValidated;
123             }
124             return _isValidated = newIsValidated;         
125         }
126 
127 		final Vec2f position() { return _position; }
128 		final Vec2f position(Vec2f newPosition) {
129             auto oldPosition = _position;
130             _position = newPosition;
131             onDeltaPosition(newPosition - oldPosition);
132             onPosition();
133             return _position;
134         }
135 
136 		final Vec2f size() const { return _size; }
137 		final Vec2f size(Vec2f newSize) {
138             auto oldSize = _size;
139             _size = newSize - _padding;
140             onDeltaSize(_size - oldSize);         
141             onSize();
142             return _size;
143         }
144 
145 		final Vec2f anchor() const { return _anchor; }
146 		final Vec2f anchor(Vec2f newAnchor) {
147             auto oldAnchor = _anchor;
148             _anchor = newAnchor;
149             onDeltaAnchor(newAnchor - oldAnchor);
150             onAnchor();
151             return _anchor;
152         }
153 
154 		final Vec2f anchoredPosition() const {
155 			return _position + _size * (Vec2f.half - _anchor);
156 		}
157 
158 		final Vec2f padding() const { return _padding; }
159 		final Vec2f padding(Vec2f newPadding) {
160             _padding = newPadding;
161 			size(_size);
162             onPadding();
163             return _padding;
164         }
165 
166 		final float angle() const { return _angle; }
167 		final float angle(float newAngle) {
168             _angle = newAngle;
169             onAngle();
170             return _angle;
171         }
172 	}
173 
174 	this() {}
175 
176 	bool isInside(const Vec2f pos) const {
177 		Vec2f collision = _size + _padding;
178 		return (_position - pos).isBetween(-collision * (Vec2f.one - anchor), collision * _anchor);
179 	}
180 
181 	bool isOnInteractableWidget(Vec2f pos) const {
182 		if(isInside(pos))
183 			return _isInteractable;
184 		return false;
185 	}
186 
187 	void setHint(string title, string text = "") {
188 		_hint = makeHint(title, text);
189 	}
190 
191 	void drawOverlay() {
192 		if(_isHovered && _hint !is null)
193 			openHintWindow(_hint);
194 
195 		if(_isWidgetDebug)
196 			drawRect(_position - _anchor * _size, _size, Color.green);
197 	}
198 
199 	void setCallback(Widget callbackWidget, string callbackId) {
200 		_callbackWidget = callbackWidget;
201 		_callbackId = callbackId;
202 	}
203 
204 	protected void triggerCallback() {
205 		if(_callbackWidget !is null) {
206 			Event ev = EventType.Callback;
207 			ev.id = _callbackId;
208 			ev.widget = this;
209 			_callbackWidget.onEvent(ev);
210 		}
211 	}
212 
213 	abstract void update(float deltaTime);
214 	abstract void onEvent(Event event);
215 	abstract void draw();
216 
217     protected {
218         void onLock() {}
219         void onMovable() {}
220         void onHover() {}
221         void onSelect() {}
222         void onFocus() {}
223         void onInteractable() {}
224         void onValidate() {}
225         void onDeltaPosition(Vec2f delta) {}
226         void onPosition() {}
227         void onDeltaSize(Vec2f delta) {}
228         void onSize() {}
229         void onDeltaAnchor(Vec2f delta) {}
230         void onAnchor() {}
231         void onPadding() {}
232         void onAngle() {}
233     }
234 }
235 
236 class WidgetGroup: Widget {
237 	protected {
238 		Widget[] _children;
239 		bool _isFrame = false;
240 
241         //Mouse control        
242 		Vec2f _lastMousePos;
243 		bool _isGrabbed = false, _isChildGrabbed = false, _isChildHovered = false;
244 		uint _idChildGrabbed;
245 
246         //Iteration
247         bool _isIterating, _isWarping = true;
248         uint _idChildIterator;
249         Timer _iteratorTimer, _iteratorTimeOutTimer;
250 	}
251 
252 	@property {
253 		const(Widget[]) children() const { return _children; }
254 		Widget[] children() { return _children; }
255 	}
256 
257 	override void update(float deltaTime) {
258         _iteratorTimer.update(deltaTime);
259         _iteratorTimeOutTimer.update(deltaTime);
260 		foreach(Widget widget; _children)
261 			widget.update(deltaTime);
262 	}
263 
264     override void onHover() {
265         if(!_isHovered) {
266             foreach(Widget widget; _children)
267                 widget.isHovered = false;
268         }
269     }
270 	
271 	override void onEvent(Event event) {
272 		switch (event.type) with(EventType) {
273 		case MouseDown:
274 			bool hasClickedWidget;
275 			foreach(uint id, Widget widget; _children) {
276 				widget.hasFocus = false;
277 				if(!widget.isInteractable)
278 					continue;
279 
280 				if(!hasClickedWidget && widget.isInside(_isFrame ? getViewVirtualPos(event.position, _position) : event.position)) {
281 					widget.hasFocus = true;
282 					widget.isSelected = true;
283 					widget.isHovered = true;
284 					_isChildGrabbed = true;
285 					_idChildGrabbed = id;
286 
287 					if(_isFrame)
288 						event.position = getViewVirtualPos(event.position, _position);
289 					widget.onEvent(event);
290 					hasClickedWidget = true;
291 				}
292 			}
293 
294 			if(!_isChildGrabbed && _isMovable) {
295 				_isGrabbed = true;
296 				_lastMousePos = event.position;
297 			}
298 			break;
299 		case MouseUp:
300 			if(_isChildGrabbed) {
301 				_isChildGrabbed = false;
302 				_children[_idChildGrabbed].isSelected = false;
303 
304 				if(_isFrame)
305 					event.position = getViewVirtualPos(event.position, _position);
306 				_children[_idChildGrabbed].onEvent(event);
307 			}
308 			else {
309 				_isGrabbed = false;
310 			}
311 			break;
312 		case MouseUpdate:
313             _isIterating = false; //Use mouse control
314 			Vec2f mousePosition = event.position;
315 			if(_isFrame)
316 				event.position = getViewVirtualPos(event.position, _position);
317 
318 			_isChildHovered = false;
319 			foreach(uint id, Widget widget; _children) {
320 				if(isHovered) {
321 					widget.isHovered = widget.isInside(event.position);
322 					if(widget.isHovered && widget.isInteractable) {
323 						_isChildHovered = true;
324 						widget.onEvent(event);
325 					}
326 				}
327 				else
328 					widget.isHovered = false;
329 			}
330 
331 			if(_isChildGrabbed && !_children[_idChildGrabbed].isHovered)
332 				_children[_idChildGrabbed].onEvent(event);
333 			else if(_isGrabbed && _isMovable) {
334 				Vec2f deltaPosition = (mousePosition - _lastMousePos);
335 				if(!_isFrame) {
336 					//Clamp the window in the screen
337 					if(isModal()) {
338 						Vec2f halfSize = _size / 2f;
339 						Vec2f clampedPosition = _position.clamp(halfSize, screenSize - halfSize);
340 						deltaPosition += (clampedPosition - _position);
341 					}
342 					_position += deltaPosition;
343 
344 					foreach(widget; _children)
345 						widget.position = widget.position + deltaPosition;
346 				}
347 				else
348 					_position += deltaPosition;
349 				_lastMousePos = mousePosition;
350 			}
351 			break;
352 		case MouseWheel:
353 			foreach(uint id, Widget widget; _children) {
354 				if(widget.isHovered)
355 					widget.onEvent(event);
356 			}
357 
358 			if(_isChildGrabbed && !_children[_idChildGrabbed].isHovered)
359 				_children[_idChildGrabbed].onEvent(event);
360 			break;
361 		case Callback:
362 			//We musn't propagate the callback further, so it's catched here.
363 			break;
364 		default:
365 			foreach(Widget widget; _children)
366 				widget.onEvent(event);
367 			break;
368 		}
369 	}
370     
371     override void onDeltaPosition(Vec2f delta) {
372         if(!_isFrame) {
373             foreach(widget; _children)
374                 widget.position = widget.position + delta;
375         }
376     }
377 
378 	override void draw() {
379 		foreach_reverse(Widget widget; _children)
380 			widget.draw();
381 	}
382 
383 	override bool isOnInteractableWidget(Vec2f pos) const {
384 		if(!isInside(pos))
385 			return false;
386 
387 		if(_isFrame)
388 			pos = getViewVirtualPos(pos, _position);
389 		
390 		foreach(const Widget widget; _children) {
391 			if(widget.isOnInteractableWidget(pos))
392 				return true;
393 		}
394 		return false;
395 	}
396 
397 	void addChild(Widget widget) {
398 		_children ~= widget;
399 	}
400 
401 	void removeChildren() {
402         _isChildGrabbed = false;
403 		_children.length = 0uL;
404 	}
405 
406 	int getChildrenCount() {
407 		return cast(int)(_children.length);
408 	}
409 
410 	void removeChild(uint id) {
411         _isChildGrabbed = false;
412         _isChildHovered = false;
413 		if(!_children.length)
414 			return;
415 		if(id + 1u == _children.length)
416 			_children.length --;
417 		else if(id == 0u)
418 			_children = _children[1..$];
419 		else
420 			_children = _children[0..id]  ~ _children[id + 1..$];
421 	}
422 	
423 	override void drawOverlay() {
424 		if(_isWidgetDebug)
425 			drawRect(_position - _anchor * _size, _size, Color.cyan);
426 
427 		if(!_isHovered)
428 			return;
429 
430 		if(_hint !is null)
431 			openHintWindow(_hint);
432 
433 		foreach(widget; _children)
434 			widget.drawOverlay();
435 	}
436 
437     //Iteration
438     private void setupIterationTimer(float time) {
439         _iteratorTimer.start(time);
440         _iteratorTimeOutTimer.start(5f);
441     }
442 
443     private bool startIteration() {
444         if(_isIterating)
445             return _iteratorTimeOutTimer.isRunning;
446         _isIterating = true;
447         
448         //If a child was hovered, we start from here
449         if(_isChildHovered) {
450             foreach(i; 0.. _children.length) {
451                 if(_children[i].isHovered) {
452                     _idChildIterator = cast(int)i;
453                     break;
454                 }
455             }
456         }
457         else
458             _idChildIterator = 0u;
459         return false;
460     }
461 
462     void stopChild() {
463         _iteratorTimeOutTimer.stop();
464     }
465 
466     void previousChild() {
467         bool wasIterating = startIteration();
468         if(_iteratorTimer.isRunning)
469             return;
470         setupIterationTimer(wasIterating ? .15f : .35f);
471         if(_idChildIterator == 0u) {
472             if(_children.length < 1)
473                 return;
474             if(_isWarping)
475                 _idChildIterator = (cast(int)_children.length) - 1;
476             else return;
477         }
478         else _idChildIterator --;
479 
480         foreach(uint id, Widget widget; _children)
481             widget.isHovered = (id == _idChildIterator);
482     }
483 
484     void nextChild() {
485         bool wasIterating = startIteration();
486         if(_iteratorTimer.isRunning)
487             return;
488         setupIterationTimer(wasIterating ? .15f : .35f);
489         if(_idChildIterator + 1u == _children.length) {
490             if(_children.length < 1)
491                 return;
492             if(_isWarping)
493                 _idChildIterator = 0u;
494             else return;
495         }
496         else _idChildIterator ++;
497 
498         foreach(uint id, Widget widget; _children)
499             widget.isHovered = (id == _idChildIterator);
500     }
501 
502     Widget selectChild() {
503         startIteration();
504 
505         if(_idChildIterator < _children.length)
506             return _children[_idChildIterator];
507         return null;
508     }
509 }