1 /** 2 Copyright: Copyright (c) 2017-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 module voxelman.gui.guicontext; 8 9 import std.stdio; 10 import std.typecons : Flag, Yes, No; 11 import datadriven; 12 import voxelman.graphics; 13 import voxelman.gui; 14 import voxelman.platform.input; 15 import voxelman.math; 16 import voxelman.text.linebuffer; 17 public import voxelman.platform.cursoricon : CursorIcon; 18 import voxelman.gui.textedit.texteditorview; 19 import voxelman.gui.textedit.lineedit; 20 21 struct GuiState 22 { 23 WidgetId draggedWidget; /// Will receive onDrag events 24 WidgetId focusedWidget; /// Will receive all key events if input is not grabbed by other widget 25 WidgetId hoveredWidget; /// Widget over which pointer is located 26 WidgetId inputOwnerWidget; /// If set, this widget will receive all pointer movement events 27 WidgetId lastClickedWidget; /// Used for double-click checking. Is set before click event distribution 28 WidgetId pressedWidget; 29 30 ivec2 canvasSize; 31 ivec2 prevPointerPos = ivec2(int.max, int.max); 32 ivec2 pointerPressPos = ivec2(int.max, int.max); 33 ivec2 curPointerPos; 34 35 /// filled with curPointerPos - draggedWidget.absPos at the moment of press 36 ivec2 draggedWidgetOffset; 37 38 /// Icon is reset after widget leave event and before widget enter event. 39 /// If widget wants to change icon, it must set cursorIcon in PointerEnterEvent handler. 40 CursorIcon cursorIcon; 41 42 string delegate() getClipboard; 43 void delegate(string) setClipboard; 44 } 45 46 struct ImplicitGuiStyle 47 { 48 import voxelman.container.chunkedbuffer; 49 FontRef defaultFont; 50 ChunkedBuffer!(FontRef, 16) fontStack; 51 52 FontRef font() { 53 if (fontStack.length) return fontStack.top; 54 return defaultFont; 55 } 56 void pushFont(FontRef font) { fontStack.push(font); } 57 void popFont() { fontStack.pop(); } 58 59 ChunkedBuffer!(Color4ub, 16) colorStack; 60 Color4ub defaultColor = Colors.black; 61 Color4ub color() { 62 if (colorStack.length) return colorStack.top; 63 return defaultColor; 64 } 65 void pushColor(Color4ub color) { colorStack.push(color); } 66 void popColor() { colorStack.pop(); } 67 68 SpriteRef iconPlaceholder; 69 SpriteRef[string] iconMap; 70 71 SpriteRef icon(string iconId) { return iconMap.get(iconId, iconPlaceholder); } 72 } 73 74 class GuiContext 75 { 76 EntityIdManager widgetIds; 77 EntityManager widgets; 78 WidgetId[string] nameToId; 79 80 /// Roots are auto-expanded on hvexpand to state.canvasSize 81 WidgetId[] roots; 82 83 GuiState state; 84 ImplicitGuiStyle style; 85 LineBuffer* debugText; 86 87 this(LineBuffer* debugText) 88 { 89 widgets.eidMan = &widgetIds; 90 voxelman.gui.widgets.registerComponents(widgets); 91 voxelman.gui.components.registerComponents(widgets); 92 voxelman.gui.textedit.texteditorview.registerComponents(widgets); 93 widgets.registerComponent!LineEditData; 94 95 roots ~= createWidget(WidgetType("root")).hvexpand; 96 roots ~= createWidget(WidgetType("windows")).hvexpand; 97 this.debugText = debugText; 98 } 99 100 WidgetProxy getRoot(size_t rootIndex) { return WidgetProxy(roots[rootIndex], this); } 101 // context menus, drop-down lists, tooltips 102 WidgetProxy createOverlay() { 103 roots ~= createWidget(WidgetType("overlay")).hvexpand; 104 return WidgetProxy(roots[$-1], this); 105 } 106 107 ChildrenRange getRoots() { return ChildrenRange(this, roots); } 108 109 // layer for dropdown items, context menus and hints 110 111 // SET, GET, HAS proxies 112 void set(Components...)(WidgetId wid, Components components) { widgets.set(wid, components); } 113 C* get(C)(WidgetId wid) { return widgets.get!C(wid); } 114 C* getOrCreate(C)(WidgetId wid, C defVal = C.init) { return widgets.getOrCreate!C(wid, defVal); } 115 bool has(C)(WidgetId wid) { return widgets.has!C(wid); } 116 void remove(C)(WidgetId wid) { widgets.remove!C(wid); } 117 118 // WIDGET METHODS 119 120 /// returns 0 if not found 121 WidgetId getByName(string name) 122 { 123 return nameToId.get(name, WidgetId(0)); 124 } 125 126 /// Pass string as first parameter to set name 127 /// Pass WidgetId as first parameter, or after string to set parent 128 /// createWidget([string name,] [WidgetId parent,] Component... components) 129 WidgetProxy createWidget(Components...)(Components components) 130 if (components.length > 0 && 131 !is(Components[0] == string) && 132 !is(Components[0] == WidgetProxy) && 133 !is(Components[0] == WidgetId)) 134 { 135 auto wid = widgetIds.nextEntityId(); 136 widgets.set(wid, components); 137 return WidgetProxy(wid, this); 138 } 139 140 /// ditto 141 WidgetProxy createWidget(Components...)(string name, Components components) 142 { 143 auto wid = widgetIds.nextEntityId(); 144 nameToId[name] = wid; 145 widgets.set(wid, WidgetName(name), components); 146 return WidgetProxy(wid, this); 147 } 148 149 /// ditto 150 WidgetProxy createWidget(Components...)(WidgetId parent, Components components) 151 { 152 auto wid = widgetIds.nextEntityId(); 153 widgets.set(wid, components); 154 addChild(parent, wid); 155 return WidgetProxy(wid, this); 156 } 157 158 /// ditto 159 WidgetProxy createWidget(Components...)(string name, WidgetId parent, Components components) 160 { 161 auto wid = widgetIds.nextEntityId(); 162 nameToId[name] = wid; 163 widgets.set(wid, WidgetName(name), components); 164 addChild(parent, wid); 165 return WidgetProxy(wid, this); 166 } 167 168 void removeWidget(WidgetId wid) 169 { 170 auto tr = widgets.getOrCreate!WidgetTransform(wid); 171 172 foreach(widget; visitTreeChildrenFirstAll(wid)) 173 widgets.remove(wid); 174 175 if (tr.parent) 176 { 177 auto container = widgets.get!WidgetContainer(tr.parent); 178 if (container) container.removeChild(wid); 179 } 180 else // check if root 181 { 182 import std.algorithm : remove, countUntil; 183 auto index = countUntil(roots, wid); 184 if (index != -1) roots = remove(roots, index); 185 } 186 } 187 188 /// Call to set parent after components are set 189 /// because it will create WidgetTransform component for child 190 /// which will be overwritten by set call if Components list 191 /// contains WidgetTransform components too. 192 void addChild(WidgetId parent, WidgetId child) 193 { 194 widgets.getOrCreate!WidgetContainer(parent).put(child); 195 widgets.getOrCreate!WidgetTransform(child).parent = parent; 196 } 197 198 WidgetId[] widgetChildren(WidgetId wid) 199 { 200 if (auto container = widgets.get!WidgetContainer(wid)) return container.children; 201 return null; 202 } 203 204 static struct WidgetTreeVisitor(bool rootFirst, bool onlyVisible) 205 { 206 WidgetId root; 207 GuiContext ctx; 208 int opApply(scope int delegate(WidgetProxy) del) 209 { 210 int visitSubtree(WidgetId root) 211 { 212 static if (rootFirst) { 213 if (auto ret = del(WidgetProxy(root, ctx))) return ret; 214 } 215 216 foreach(child; ctx.widgetChildren(root)) 217 { 218 static if (onlyVisible) { 219 if (ctx.widgets.has!hidden(child)) continue; 220 } 221 if (auto ret = visitSubtree(child)) 222 return ret; 223 } 224 225 static if (!rootFirst) { 226 if (auto ret = del(WidgetProxy(root, ctx))) return ret; 227 } 228 229 return 0; 230 } 231 232 return visitSubtree(root); 233 } 234 } 235 236 auto visitTreeRootFirstVisible(WidgetId root) { return WidgetTreeVisitor!(true, true)(root, this); } 237 auto visitTreeChildrenFirstVisible(WidgetId root) { return WidgetTreeVisitor!(false, true)(root, this); } 238 auto visitTreeRootFirstAll(WidgetId root) { return WidgetTreeVisitor!(true, false)(root, this); } 239 auto visitTreeChildrenFirstAll(WidgetId root) { return WidgetTreeVisitor!(false, false)(root, this); } 240 241 bool postEvent(Event)(WidgetId wid, auto ref Event event) 242 { 243 if (auto events = widgets.get!WidgetEvents(wid)) 244 return events.postEvent(WidgetProxy(wid, this), event); 245 return false; 246 } 247 248 bool postEvent(Event)(WidgetProxy widget, auto ref Event event) 249 { 250 if (auto events = widget.get!WidgetEvents) 251 return events.postEvent(widget, event); 252 return false; 253 } 254 255 static bool containsPointer(WidgetId widget, GuiContext context, ivec2 pointerPos) 256 { 257 auto transform = context.widgets.getOrCreate!WidgetTransform(widget); 258 return transform.contains(pointerPos); 259 } 260 261 262 // EVENT HANDLERS 263 264 void onScroll(dvec2 delta) 265 { 266 auto event = ScrollEvent(vec2(-delta)); 267 268 foreach_reverse(root; roots) 269 { 270 WidgetId[] path = buildPathToLeaf!(containsPointer)(this, root, this, state.curPointerPos); 271 WidgetId[] eventConsumerChain = propagateEventSinkBubble(this, path, event, OnHandle.StopTraversing); 272 } 273 updateHovered(state.curPointerPos); 274 } 275 276 void onKeyPress(KeyCode key, uint modifiers) 277 { 278 if (focusedWidget) 279 { 280 auto event = KeyPressEvent(key, modifiers); 281 postEvent(focusedWidget, event); 282 } 283 } 284 285 void onKeyRelease(KeyCode key, uint modifiers) 286 { 287 if (focusedWidget) 288 { 289 auto event = KeyReleaseEvent(key, modifiers); 290 postEvent(focusedWidget, event); 291 } 292 } 293 294 void onCharEnter(dchar character) 295 { 296 if (focusedWidget) 297 { 298 auto event = CharEnterEvent(character); 299 postEvent(focusedWidget, event); 300 } 301 } 302 303 void pointerPressed(PointerButton button, uint modifiers) 304 { 305 auto event = PointerPressEvent(state.curPointerPos, button, modifiers); 306 state.pointerPressPos = state.curPointerPos; 307 308 foreach_reverse(root; roots) 309 { 310 WidgetId[] path = buildPathToLeaf!(containsPointer)(this, root, this, state.curPointerPos); 311 WidgetId[] eventConsumerChain = propagateEventSinkBubble(this, path, event, OnHandle.StopTraversing); 312 313 if (eventConsumerChain.length > 0) 314 { 315 WidgetId consumer = eventConsumerChain[$-1]; 316 if (widgets.has!WidgetIsFocusable(consumer)) 317 focusedWidget = consumer; 318 319 if (event.beginDrag) beginDrag(consumer); 320 321 pressedWidget = consumer; 322 return; 323 } 324 } 325 326 focusedWidget = WidgetId(0); 327 } 328 329 void pointerReleased(PointerButton button, uint modifiers) 330 { 331 auto event = PointerReleaseEvent(state.curPointerPos, button, modifiers); 332 scope(exit) pressedWidget = WidgetId(0); 333 334 if (draggedWidget) 335 { 336 endDrag(); 337 return; 338 } 339 340 foreach_reverse(root; roots) 341 { 342 WidgetId[] path = buildPathToLeaf!(containsPointer)(this, root, this, state.curPointerPos); 343 344 foreach_reverse(item; path) // test if pointer over pressed widget. 345 { 346 if (item == pressedWidget) 347 { 348 WidgetId[] eventConsumerChain = propagateEventSinkBubble(this, path, event, OnHandle.StopTraversing); 349 350 if (eventConsumerChain.length > 0) 351 { 352 if (pressedWidget == eventConsumerChain[$-1]) 353 { 354 auto clickEvent = PointerClickEvent(state.curPointerPos, button); 355 lastClickedWidget = pressedWidget; 356 postEvent(pressedWidget, clickEvent); 357 } 358 } 359 360 return; 361 } 362 } 363 } 364 365 if (pressedWidget) // no one handled event. Let's pressed widget know that pointer was released. 366 { 367 postEvent(pressedWidget, event); // pressed widget will know if pointer was unpressed somewhere else. 368 updateHovered(state.curPointerPos); // So widget knows if pointer released not over it. 369 } 370 } 371 372 void pointerMoved(ivec2 newPointerPos) 373 { 374 if (newPointerPos == state.curPointerPos) return; 375 376 state.prevPointerPos = state.curPointerPos; 377 state.curPointerPos = newPointerPos; 378 ivec2 delta = state.curPointerPos - state.prevPointerPos; 379 380 auto event = PointerMoveEvent(newPointerPos, delta); 381 382 if (draggedWidget) 383 { 384 doDrag(delta); 385 } 386 else if (pressedWidget) 387 { 388 if (containsPointer(pressedWidget, this, state.curPointerPos)) 389 { 390 postEvent(pressedWidget, event); 391 if (event.handled) 392 { 393 hoveredWidget = pressedWidget; 394 return; 395 } 396 } 397 } 398 else 399 { 400 if (updateHovered(newPointerPos, delta)) return; 401 } 402 403 hoveredWidget = WidgetId(0); 404 } 405 406 private bool updateHovered(ivec2 pointerPos, ivec2 delta = ivec2(0,0)) 407 { 408 foreach_reverse(root; roots) 409 { 410 WidgetId[] path = buildPathToLeaf!(containsPointer)(this, root, this, pointerPos); 411 auto event = PointerMoveEvent(pointerPos, delta); 412 413 foreach_reverse(widget; path) 414 { 415 postEvent(widget, event); 416 if (event.handled) 417 { 418 hoveredWidget = widget; 419 return true; 420 } 421 } 422 } 423 424 hoveredWidget = WidgetId(0); 425 return false; 426 } 427 428 void update(double deltaTime, RenderQueue renderQueue) 429 { 430 foreach(root; roots) 431 propagateEventSinkBubbleTree!(No.CheckHidden)(this, root, GuiUpdateEvent(deltaTime)); 432 433 updateLayout(); 434 435 auto drawEvent = DrawEvent(renderQueue); 436 foreach(root; roots) 437 propagateEventSinkBubbleTree(this, root, drawEvent); 438 } 439 440 private void updateLayout() 441 { 442 foreach(root; roots) 443 { 444 MeasureEvent measureEvent; 445 foreach(widget; visitTreeChildrenFirstVisible(root)) 446 { 447 postEvent(widget, measureEvent); 448 auto childTransform = widgets.getOrCreate!WidgetTransform(widget); 449 childTransform.size = childTransform.constrainedSize; 450 } 451 452 auto trans = widgets.getOrCreate!WidgetTransform(root); 453 trans.size = trans.constrainedSize; 454 if (trans.hasHexpand) trans.size.x = state.canvasSize.x; 455 if (trans.hasVexpand) trans.size.y = state.canvasSize.y; 456 trans.absPos = trans.relPos; 457 458 LayoutEvent layoutEvent; 459 foreach(widget; visitTreeRootFirstVisible(root)) 460 { 461 postEvent(widget, layoutEvent); 462 auto parentTransform = widget.getOrCreate!WidgetTransform; 463 foreach(child; widget.children) 464 { 465 auto childTransform = child.getOrCreate!WidgetTransform; 466 childTransform.absPos = parentTransform.absPos + childTransform.relPos; 467 childTransform.size.x = max(childTransform.size.x, 0); 468 childTransform.size.y = max(childTransform.size.y, 0); 469 } 470 } 471 } 472 } 473 474 // STATE 475 476 string clipboard() 477 { 478 return state.getClipboard(); 479 } 480 481 void clipboard(S)(S str) 482 { 483 import std.array : array; 484 import std.utf : byChar; 485 state.setClipboard(cast(string)str.byChar.array); 486 } 487 488 void cursorIcon(CursorIcon icon) { state.cursorIcon = icon; } 489 490 void beginDrag(WidgetId wid) 491 { 492 draggedWidget = wid; 493 state.draggedWidgetOffset = state.curPointerPos - get!WidgetTransform(wid).absPos; 494 postEvent(wid, DragBeginEvent()); 495 } 496 497 void doDrag(ivec2 delta) 498 { 499 assert(draggedWidget); 500 postEvent(draggedWidget, DragEvent(delta)); 501 } 502 503 void endDrag() 504 { 505 assert(draggedWidget); 506 postEvent(draggedWidget, DragEndEvent()); 507 draggedWidget = WidgetId(0); 508 updateHovered(state.curPointerPos); 509 } 510 511 WidgetId draggedWidget() { return state.draggedWidget; } 512 void draggedWidget(WidgetId wid) { state.draggedWidget = wid; updateHovered(state.curPointerPos); } 513 514 WidgetId focusedWidget() { return state.focusedWidget; } 515 void focusedWidget(WidgetId wid) 516 { 517 if (state.focusedWidget != wid) 518 { 519 if (state.focusedWidget) postEvent(state.focusedWidget, FocusLoseEvent()); 520 if (wid) postEvent(wid, FocusGainEvent()); 521 state.focusedWidget = wid; 522 } 523 } 524 525 WidgetId hoveredWidget() { return state.hoveredWidget; } 526 void hoveredWidget(WidgetId wid) @trusted 527 { 528 if (state.hoveredWidget != wid) 529 { 530 if (state.hoveredWidget) postEvent(state.hoveredWidget, PointerLeaveEvent()); 531 cursorIcon = CursorIcon.arrow; 532 if (wid) postEvent(wid, PointerEnterEvent()); 533 state.hoveredWidget = wid; 534 } 535 } 536 537 WidgetId inputOwnerWidget() { return state.inputOwnerWidget; } 538 void inputOwnerWidget(WidgetId wid) { state.inputOwnerWidget = wid; } 539 540 WidgetId lastClickedWidget() { return state.lastClickedWidget; } 541 void lastClickedWidget(WidgetId wid) { state.lastClickedWidget = wid; } 542 543 WidgetId pressedWidget() { return state.pressedWidget; } 544 void pressedWidget(WidgetId wid) { state.pressedWidget = wid; updateHovered(state.curPointerPos); } 545 }