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 }