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.widgets; 8 9 import std.stdio; 10 import voxelman.gui; 11 import voxelman.math; 12 import voxelman.graphics; 13 import datadriven.entityman : EntityManager; 14 15 void registerComponents(ref EntityManager widgets) 16 { 17 widgets.registerComponent!ButtonState; 18 widgets.registerComponent!ChildrenStash; 19 widgets.registerComponent!ConditionData; 20 widgets.registerComponent!DraggableSettings; 21 widgets.registerComponent!DropDownData; 22 widgets.registerComponent!IconData; 23 widgets.registerComponent!ImageData; 24 widgets.registerComponent!LinearLayoutSettings; 25 widgets.registerComponent!ListData; 26 widgets.registerComponent!ScrollableData; 27 widgets.registerComponent!SingleLayoutSettings; 28 widgets.registerComponent!TextData; 29 widgets.registerComponent!UserCheckHandler; 30 widgets.registerComponent!UserClickHandler; 31 widgets.registerComponent!WidgetIndex; 32 widgets.registerComponent!WidgetReference; 33 } 34 35 @Component("gui.WidgetReference", Replication.none) 36 struct WidgetReference 37 { 38 WidgetId widgetId; 39 } 40 41 struct WidgetProxy 42 { 43 WidgetId wid; 44 GuiContext ctx; 45 46 alias wid this; 47 48 WidgetProxy set(Components...)(Components components) { ctx.widgets.set(wid, components); return this; } 49 C* get(C)() { return ctx.widgets.get!C(wid); } 50 C* getOrCreate(C)(C defVal = C.init) { return ctx.widgets.getOrCreate!C(wid, defVal); } 51 bool has(C)() { return ctx.widgets.has!C(wid); } 52 WidgetProxy remove(C)() { ctx.widgets.remove!C(wid); return this; } 53 WidgetProxy createChild(Components...)(Components components) { return ctx.createWidget(wid, components); } 54 WidgetProxy handlers(Handlers...)(Handlers h) { ctx.widgets.getOrCreate!WidgetEvents(wid).addEventHandlers(h); return this; } 55 void addChild(WidgetId child) { ctx.addChild(wid, child); } 56 bool postEvent(Event)(auto ref Event event) { return ctx.postEvent(this, event); } 57 void toggleFlag(Component)() { if (ctx.widgets.has!Component(wid)) ctx.widgets.remove!Component(wid); else ctx.widgets.set(wid, Component()); } 58 59 void focus() { ctx.focusedWidget = wid; } 60 void unfocus() { ctx.focusedWidget = 0; } 61 void setFocus(bool isFocused) { if (isFocused) ctx.focusedWidget = wid; else ctx.focusedWidget = 0; } 62 } 63 64 static struct ChildrenRange 65 { 66 GuiContext ctx; 67 WidgetId[] children; 68 size_t length(){ return children.length; } 69 WidgetProxy opIndex(size_t i) { return WidgetProxy(children[i], ctx); } 70 int opApply(scope int delegate(WidgetProxy) del) 71 { 72 foreach(childId; children) 73 { 74 if (ctx.widgets.has!hidden(childId)) continue; 75 if (auto ret = del(WidgetProxy(childId, ctx))) 76 return ret; 77 } 78 return 0; 79 } 80 } 81 82 83 enum baseColor = rgb(26, 188, 156); 84 enum hoverColor = rgb(22, 160, 133); 85 enum color_clouds = rgb(236, 240, 241); 86 enum color_silver = rgb(189, 195, 199); 87 enum color_concrete = rgb(149, 165, 166); 88 enum color_asbestos = rgb(127, 140, 141); 89 enum color_white = rgb(250, 250, 250); 90 enum color_gray = rgb(241, 241, 241); 91 92 enum color_wet_asphalt = rgb(52, 73, 94); 93 94 struct FrameParts 95 { 96 WidgetProxy frame; 97 alias frame this; 98 WidgetProxy header; 99 WidgetProxy container; 100 } 101 102 struct Frame 103 { 104 static: 105 FrameParts create(WidgetProxy parent) 106 { 107 WidgetProxy frame = parent.createChild(WidgetType("Frame")) 108 .setVLayout(0, padding4(0)) 109 .addBackground(color_clouds) 110 .consumeMouse; 111 112 auto header = frame.createChild(WidgetType("Header")) 113 .addBackground(color_white) 114 .hexpand; 115 116 auto container = frame.createChild(WidgetType("Container")).hvexpand; 117 118 return FrameParts(frame, header, container); 119 } 120 } 121 122 @Component("gui.ConditionData", Replication.none) 123 struct ConditionData 124 { 125 bool delegate() condition; 126 bool invert; 127 } 128 129 WidgetProxy visible_if(WidgetProxy widget, bool delegate() condition) 130 { 131 widget.set(ConditionData(condition)).handlers(&updateVisibility); return widget; 132 } 133 134 WidgetProxy visible_if_not(WidgetProxy widget, bool delegate() condition) 135 { 136 widget.set(ConditionData(condition, true)).handlers(&updateVisibility); return widget; 137 } 138 139 void updateVisibility(WidgetProxy widget, ref GuiUpdateEvent event) 140 { 141 if (event.bubbling) return; 142 auto data = widget.get!ConditionData; 143 if (data.condition is null) return; 144 bool isVisible = data.condition(); 145 if (data.invert) isVisible = !isVisible; 146 if (isVisible) 147 widget.remove!hidden; 148 else 149 widget.set(hidden()); 150 } 151 152 WidgetProxy addBackground(WidgetProxy widget, Color4ub color) 153 { 154 widget.getOrCreate!WidgetStyle.color = color; 155 widget.handlers(&PanelLogic.drawBackground); 156 return widget; 157 } 158 159 WidgetProxy addBorder(WidgetProxy widget, Color4ub color) 160 { 161 widget.getOrCreate!WidgetStyle.borderColor = color; 162 widget.handlers(&PanelLogic.drawBorder); 163 return widget; 164 } 165 166 struct PanelLogic 167 { 168 static: 169 WidgetProxy create(WidgetProxy parent, Color4ub color) 170 { 171 WidgetProxy panel = parent.createChild( 172 WidgetEvents(&drawBackground), 173 WidgetStyle(color), WidgetType("Panel")); 174 return panel; 175 } 176 177 void drawBackground(WidgetProxy widget, ref DrawEvent event) 178 { 179 if (event.sinking) { 180 auto transform = widget.getOrCreate!WidgetTransform; 181 auto style = widget.get!WidgetStyle; 182 event.renderQueue.drawRectFill(vec2(transform.absPos), vec2(transform.size), event.depth, style.color); 183 event.depth += 1; 184 event.renderQueue.pushClipRect(irect(transform.absPos, transform.size)); 185 } else { 186 event.renderQueue.popClipRect(); 187 } 188 } 189 190 void drawBorder(WidgetProxy widget, ref DrawEvent event) 191 { 192 if (event.sinking) { 193 auto transform = widget.getOrCreate!WidgetTransform; 194 auto style = widget.get!WidgetStyle; 195 event.renderQueue.drawRectLine(vec2(transform.absPos), vec2(transform.size), event.depth, style.borderColor); 196 event.depth += 1; 197 } 198 } 199 } 200 201 WidgetProxy createImage(WidgetProxy parent, ImageData data) 202 { 203 return ImageLogic.create(parent, data); 204 } 205 206 @Component("gui.ImageData", Replication.none) 207 struct ImageData 208 { 209 Texture texture; 210 irect subRect; 211 int scale; 212 Color4ub color = Colors.white; 213 } 214 215 struct ImageLogic 216 { 217 static: 218 WidgetProxy create(WidgetProxy parent, ImageData data) 219 { 220 WidgetProxy image = parent.createChild( 221 WidgetEvents(&measure, &drawWidget), data, WidgetType("Image")); 222 return image; 223 } 224 225 void measure(WidgetProxy widget, ref MeasureEvent event) 226 { 227 auto transform = widget.get!WidgetTransform; 228 auto data = widget.get!ImageData; 229 transform.measuredSize = data.subRect.size * data.scale; 230 } 231 232 void drawWidget(WidgetProxy image, ref DrawEvent event) 233 { 234 if (event.sinking) { 235 auto transform = image.getOrCreate!WidgetTransform; 236 auto data = image.get!ImageData; 237 event.renderQueue.texBatch.putRect(frect(transform.absPos, transform.size), frect(data.subRect), event.depth, data.color, data.texture); 238 event.depth += 1; 239 } 240 } 241 } 242 243 @Component("gui.IconData", Replication.none) 244 struct IconData 245 { 246 SpriteRef sprite; 247 Color4ub color; 248 Alignment halign; 249 Alignment valign; 250 } 251 252 WidgetProxy createIcon(WidgetProxy parent, string iconId, ivec2 size, Color4ub color = Colors.white) 253 { 254 return IconLogic.create(parent, parent.ctx.style.icon(iconId), color).minSize(size); 255 } 256 257 WidgetProxy createIcon(WidgetProxy parent, SpriteRef sprite, ivec2 size, Color4ub color = Colors.white) 258 { 259 return IconLogic.create(parent, sprite, color).minSize(size); 260 } 261 262 WidgetProxy createIcon(WidgetProxy parent, SpriteRef sprite, Color4ub color = Colors.white, 263 Alignment halign = Alignment.center, Alignment valign = Alignment.center) 264 { 265 return IconLogic.create(parent, sprite, color, halign, valign); 266 } 267 268 struct IconLogic 269 { 270 static: 271 WidgetProxy create(WidgetProxy parent, SpriteRef sprite, Color4ub color = Colors.white, 272 Alignment halign = Alignment.center, Alignment valign = Alignment.center) 273 { 274 WidgetProxy icon = parent.createChild( 275 WidgetEvents(&drawWidget), IconData(sprite, color, halign, valign), WidgetType("Icon")) 276 .measuredSize(sprite.atlasRect.size); 277 return icon; 278 } 279 280 void drawWidget(WidgetProxy icon, ref DrawEvent event) 281 { 282 if (event.sinking) { 283 auto transform = icon.getOrCreate!WidgetTransform; 284 auto data = icon.get!IconData; 285 auto alignmentOffset = rectAlignmentOffset(transform.measuredSize, data.halign, data.valign, transform.size); 286 event.renderQueue.draw(*data.sprite, vec2(transform.absPos+alignmentOffset), event.depth, data.color); 287 event.depth += 1; 288 } 289 } 290 } 291 292 @Component("gui.WidgetIndex", Replication.none) 293 struct WidgetIndex 294 { 295 size_t index; 296 WidgetId master; 297 } 298 299 /// Used for widgets that hide some of the children, to store original list of children 300 @Component("gui.ChildrenStash", Replication.none) 301 struct ChildrenStash 302 { 303 WidgetId[] widgets; 304 } 305 306 struct PagedWidget 307 { 308 static: 309 // Move all children to 310 void convert(WidgetProxy widget, size_t initialIndex) 311 { 312 if (auto cont = widget.get!WidgetContainer) 313 { 314 WidgetId[] pages = cont.children; 315 widget.set(ChildrenStash(pages), WidgetEvents(&measure, &layout)); 316 cont.children = null; 317 if (pages) 318 { 319 cont.put(pages[initialIndex]); 320 } 321 } 322 } 323 324 void switchPage(WidgetProxy widget, size_t newPage) 325 { 326 if (auto cont = widget.get!WidgetContainer) 327 { 328 auto pages = widget.get!ChildrenStash.widgets; 329 if (newPage < pages.length) 330 cont.children[0] = pages[newPage]; 331 } 332 } 333 334 void attachToButton(WidgetProxy selectorButton, size_t index) 335 { 336 selectorButton.set(WidgetIndex(index)); 337 selectorButton.getOrCreate!WidgetEvents.addEventHandler(&onButtonClick); 338 } 339 340 void onButtonClick(WidgetProxy widget, ref PointerClickEvent event) 341 { 342 auto data = widget.get!UserClickHandler; 343 data.onClick(); 344 } 345 346 void measure(WidgetProxy widget, ref MeasureEvent event) 347 { 348 auto transform = widget.get!WidgetTransform; 349 foreach(child; widget.children) 350 { 351 auto childTransform = child.get!WidgetTransform; 352 transform.measuredSize = childTransform.size; 353 } 354 } 355 356 void layout(WidgetProxy widget, ref LayoutEvent event) 357 { 358 auto transform = widget.get!WidgetTransform; 359 foreach(child; widget.children) 360 { 361 auto childTransform = child.get!WidgetTransform; 362 childTransform.relPos = ivec2(0,0); 363 childTransform.size = transform.size; 364 } 365 } 366 } 367 368 struct CollapsableParts 369 { 370 WidgetProxy collapsable; 371 alias collapsable this; 372 WidgetProxy header; 373 WidgetProxy container; 374 } 375 376 /// On user click toggles 377 struct CollapsableWidget 378 { 379 static: 380 CollapsableParts create(WidgetProxy parent, bool expanded = false) 381 { 382 auto collapsable = parent.createChild( 383 WidgetType("Collapsable")).hexpand; 384 VLayout.attachTo(collapsable, 2, padding4(0)); 385 386 auto header = collapsable.createChild( 387 WidgetType("Header"), 388 ButtonState(), 389 WidgetEvents(&onHeaderClick, &drawButtonStateBack, &pointerMoved, &pointerPressed, 390 &pointerReleased, &enterWidget, &leaveWidget)).hexpand; 391 392 auto container = collapsable.createChild().hexpand; 393 394 if (!expanded) toggle(collapsable); 395 return CollapsableParts(collapsable, header, container); 396 } 397 398 void onHeaderClick(WidgetProxy header, ref PointerClickEvent event) 399 { 400 auto tran = header.get!WidgetTransform; 401 toggle(WidgetProxy(tran.parent, header.ctx)); 402 } 403 404 void toggle(WidgetProxy collapsable) 405 { 406 collapsable.children[1].toggleFlag!hidden; 407 } 408 409 mixin ButtonPointerLogic!ButtonState; 410 } 411 412 @Component("gui.TextData", Replication.none) 413 struct TextData 414 { 415 string text; 416 Alignment halign; 417 Alignment valign; 418 Color4ub color; 419 } 420 421 WidgetProxy createText(WidgetProxy parent, string text, 422 Alignment halign = Alignment.center, Alignment valign = Alignment.center) 423 { 424 return TextLogic.create(parent, text, 425 parent.ctx.style.font, 426 parent.ctx.style.color, 427 halign, valign); 428 } 429 430 struct TextLogic 431 { 432 static: 433 WidgetProxy create( 434 WidgetProxy parent, 435 string text, 436 FontRef font, 437 Color4ub color, 438 Alignment halign, 439 Alignment valign) 440 { 441 WidgetProxy textWidget = parent.createChild( 442 TextData(text, halign, valign, color), 443 WidgetEvents(&drawText), 444 WidgetType("Text")) 445 .minSize(0, font.metrics.height); 446 setText(textWidget, text); 447 return textWidget; 448 } 449 450 void setText(WidgetProxy widget, string text) 451 { 452 auto data = widget.get!TextData; 453 data.text = text; 454 455 TextMesherParams params; 456 params.font = widget.ctx.style.font; 457 params.monospaced = false; 458 measureText(params, text); 459 460 widget.measuredSize(ivec2(params.size)); 461 } 462 463 void drawText(WidgetProxy widget, ref DrawEvent event) 464 { 465 if (event.bubbling) return; 466 467 auto data = widget.get!TextData; 468 auto transform = widget.getOrCreate!WidgetTransform; 469 auto alignmentOffset = rectAlignmentOffset(transform.measuredSize, data.halign, data.valign, transform.size); 470 471 auto params = event.renderQueue.startTextAt(vec2(transform.absPos)); 472 params.monospaced = false; 473 params.depth = event.depth; 474 params.color = data.color; 475 params.origin += alignmentOffset; 476 params.meshText(data.text); 477 478 event.depth += 1; 479 } 480 } 481 482 enum BUTTON_PRESSED = 0b0001; 483 enum BUTTON_HOVERED = 0b0010; 484 enum BUTTON_SELECTED = 0b0100; 485 486 enum buttonNormalColor = rgb(255, 255, 255); 487 enum buttonHoveredColor = rgb(241, 241, 241); 488 enum buttonPressedColor = rgb(229, 229, 229); 489 enum buttonSelectedColor = rgb(229, 229, 255); 490 Color4ub[8] buttonColors = [ 491 buttonNormalColor, buttonNormalColor, 492 buttonHoveredColor, buttonPressedColor, 493 buttonSelectedColor, buttonSelectedColor, 494 buttonSelectedColor, buttonSelectedColor]; 495 496 @Component("gui.ButtonState", Replication.none) 497 struct ButtonState 498 { 499 uint data; 500 bool pressed() { return (data & BUTTON_PRESSED) != 0; } 501 bool hovered() { return (data & BUTTON_HOVERED) != 0; } 502 bool selected() { return (data & BUTTON_SELECTED) != 0; } 503 void toggleSelected() { data = data.toggle_flag(BUTTON_SELECTED); } 504 } 505 506 WidgetProxy createIconTextButton(WidgetProxy parent, SpriteRef icon, string text, ClickHandler handler = null) { 507 return IconTextButtonLogic.create(parent, icon, text, handler); 508 } 509 WidgetProxy createIconTextButton(WidgetProxy parent, string iconId, string text, ClickHandler handler = null) { 510 return IconTextButtonLogic.create(parent, parent.ctx.style.icon(iconId), text, handler); 511 } 512 513 struct IconTextButtonLogic 514 { 515 static: 516 WidgetProxy create(WidgetProxy parent, SpriteRef icon, string text, ClickHandler handler = null) 517 { 518 WidgetProxy button = parent.createChild( 519 UserClickHandler(), ButtonState(), 520 WidgetEvents( 521 &drawButtonStateBack, &pointerMoved, &pointerPressed, 522 &pointerReleased, &enterWidget, &leaveWidget), 523 WidgetType("IconTextButton")) 524 .setHLayout(2, padding4(2), Alignment.center); 525 526 button.createIcon(icon, ivec2(16, 16), Colors.black); 527 button.createText(text); 528 setHandler(button, handler); 529 530 return button; 531 } 532 533 mixin ButtonPointerLogic!ButtonState; 534 mixin ButtonClickLogic!UserClickHandler; 535 } 536 537 WidgetProxy createTextButton(WidgetProxy parent, string text, ClickHandler handler = null) { 538 return TextButtonLogic.create(parent, text, handler); 539 } 540 541 struct TextButtonLogic 542 { 543 static: 544 WidgetProxy create(WidgetProxy parent, string text, ClickHandler handler = null) 545 { 546 WidgetProxy button = parent.createChild( 547 UserClickHandler(), ButtonState(), 548 WidgetEvents( 549 &drawButtonStateBack, &pointerMoved, &pointerPressed, 550 &pointerReleased, &enterWidget, &leaveWidget), 551 WidgetType("TextButton")); 552 553 button.createText(text); 554 setHandler(button, handler); 555 SingleLayout.attachTo(button, 2); 556 557 return button; 558 } 559 560 mixin ButtonPointerLogic!ButtonState; 561 mixin ButtonClickLogic!UserClickHandler; 562 } 563 564 void drawButtonStateBack(WidgetProxy widget, ref DrawEvent event) 565 { 566 if (event.bubbling) return; 567 568 auto state = widget.get!ButtonState; 569 auto transform = widget.getOrCreate!WidgetTransform; 570 571 event.renderQueue.drawRectFill(vec2(transform.absPos), vec2(transform.size), event.depth, buttonColors[state.data & 0b111]); 572 //event.renderQueue.drawRectLine(vec2(transform.absPos), vec2(transform.size), event.depth+1, rgb(230,230,230)); 573 event.depth += 1; 574 } 575 576 /// Assumes that parent has ToggleButtonData data and uses its data and isChecked fields 577 struct CheckIconLogic 578 { 579 static: 580 WidgetProxy create(WidgetProxy parent, ivec2 size) 581 { 582 WidgetTransform t; 583 t.measuredSize = size; 584 return parent.createChild(t, WidgetEvents(&drawWidget), WidgetType("CheckIcon")); 585 } 586 587 void drawWidget(WidgetProxy widget, ref DrawEvent event) 588 { 589 if (event.bubbling) return; 590 591 auto tran = widget.getOrCreate!WidgetTransform; 592 auto parentData = widget.ctx.get!UserCheckHandler(tran.parent); 593 auto parentState = widget.ctx.get!ButtonState(tran.parent); 594 595 event.renderQueue.drawRectFill(vec2(tran.absPos), vec2(tran.size), event.depth, buttonColors[parentState.data & 0b11]); 596 if (parentData.isChecked) 597 event.renderQueue.drawRectFill(vec2(tran.absPos + 2), vec2(tran.size - 4), event.depth+1, color_wet_asphalt); 598 event.renderQueue.drawRectLine(vec2(tran.absPos), vec2(tran.size), event.depth+1, color_wet_asphalt); 599 event.depth += 2; 600 } 601 } 602 603 WidgetProxy createCheckButton(WidgetProxy parent, string text, CheckHandler handler = null) { 604 return CheckButtonLogic.create(parent, text, handler); 605 } 606 607 WidgetProxy createCheckButton(WidgetProxy parent, string text, bool* flag) { 608 alias Del = ref bool delegate(); 609 alias Fun = ref bool function(); 610 Del dg; 611 dg.ptr = flag; 612 dg.funcptr = cast(Fun)&bool_delegate; 613 return CheckButtonLogic.create(parent, text, dg); 614 } 615 616 ref bool bool_delegate(bool* value) 617 { 618 return *value; 619 } 620 621 struct CheckButtonLogic 622 { 623 static: 624 WidgetProxy create(WidgetProxy parent, string text, CheckHandler handler = null) 625 { 626 WidgetProxy check = parent.createChild( 627 UserCheckHandler(), ButtonState(), 628 WidgetEvents(&pointerMoved, &pointerPressed, &pointerReleased, &enterWidget, &leaveWidget), 629 WidgetType("CheckButton")); 630 631 auto iconSize = parent.ctx.style.font.metrics.height; 632 auto icon = CheckIconLogic.create(check, ivec2(iconSize, iconSize)); 633 634 check.createText(text); 635 636 setHandler(check, handler); 637 HLayout.attachTo(check, 2, padding4(2)); 638 639 return check; 640 } 641 642 mixin ButtonPointerLogic!ButtonState; 643 mixin ButtonClickLogic!UserCheckHandler; 644 } 645 646 alias OptionSelectHandler = void delegate(size_t); 647 @Component("gui.DropDownData", Replication.none) 648 struct DropDownData 649 { 650 OptionSelectHandler handler; 651 void onClick(size_t index) { 652 if (selectedOption == index) return; 653 selectedOption = index; 654 if (handler) handler(selectedOption); 655 } 656 string[] options; 657 size_t selectedOption; 658 string optionText() { return options[selectedOption]; } 659 } 660 661 struct DropDown 662 { 663 static: 664 WidgetProxy create(WidgetProxy parent, string[] options, size_t selectedOption, OptionSelectHandler handler = null) 665 { 666 WidgetProxy dropdown = BaseButton.create(parent) 667 .handlers(&drawButtonStateBack, &onWidgetClick) 668 .set( 669 WidgetType("DropDown"), 670 DropDownData(handler, options, selectedOption)) 671 .setHLayout(0, padding4(2), Alignment.center); 672 673 dropdown.createText(options[selectedOption]); 674 dropdown.hfill; 675 dropdown.createIcon(parent.ctx.style.icon("arrow-up-down"), ivec2(16, 16), Colors.black); 676 677 return dropdown; 678 } 679 680 void onWidgetClick(WidgetProxy widget, ref PointerClickEvent event) 681 { 682 toggleDropDown(widget); 683 } 684 685 void toggleDropDown(WidgetProxy widget) 686 { 687 auto tr = widget.get!WidgetTransform; 688 auto data = widget.get!DropDownData; 689 auto state = widget.get!ButtonState; 690 state.toggleSelected; 691 692 if (state.selected) 693 { 694 auto optionsOverlay = widget.ctx.createOverlay 695 .consumeMouse 696 .handlers(&onOverlayPress) 697 .set(WidgetReference(widget)); 698 699 widget.set(WidgetReference(optionsOverlay)); 700 701 auto options = optionsOverlay.createChild() 702 .pos(tr.absPos+ivec2(0, tr.size.y)) 703 .addBackground(color_gray) 704 .minSize(tr.size.x, 0) 705 .setVLayout(2, padding4(2)); 706 707 foreach(i, option; data.options) 708 { 709 auto button = BaseButton.create(options) 710 .set(WidgetIndex(i, widget), WidgetType("DropDownOption")) 711 .handlers(&onOptionClick, &drawButtonStateBack) 712 .hexpand 713 .setSingleLayout(2, Alignment.min); 714 button.createText(option); 715 } 716 } 717 else 718 { 719 auto overlayRef = widget.get!WidgetReference; 720 widget.ctx.removeWidget(overlayRef.widgetId); 721 widget.remove!WidgetReference; 722 } 723 } 724 725 void onOverlayPress(WidgetProxy overlay, ref PointerPressEvent event) 726 { 727 if (event.sinking) return; 728 auto dropdownRef = overlay.get!WidgetReference; 729 toggleDropDown(WidgetProxy(dropdownRef.widgetId, overlay.ctx)); 730 event.handled = true; 731 } 732 733 void onOptionClick(WidgetProxy option, ref PointerClickEvent event) 734 { 735 auto index = option.get!WidgetIndex; 736 auto dropdown = WidgetProxy(index.master, option.ctx); 737 738 auto data = dropdown.get!DropDownData; 739 data.onClick(index.index); 740 toggleDropDown(dropdown); 741 TextLogic.setText(dropdown.children[0], data.optionText); 742 } 743 744 mixin ButtonPointerLogic!ButtonState; 745 } 746 747 struct BaseButton 748 { 749 static: 750 WidgetProxy create(WidgetProxy parent) 751 { 752 return parent.createChild(ButtonState(), 753 WidgetEvents( 754 &pointerMoved, &pointerPressed, 755 &pointerReleased, &enterWidget, &leaveWidget), 756 WidgetType("BaseButton")); 757 } 758 mixin ButtonPointerLogic!ButtonState; 759 } 760 761 mixin template ButtonPointerLogic(State) 762 { 763 static: 764 void pointerMoved(WidgetProxy widget, ref PointerMoveEvent event) { event.handled = true; } 765 766 void pointerPressed(WidgetProxy widget, ref PointerPressEvent event) 767 { 768 if (event.sinking) return; 769 widget.get!State.data |= BUTTON_PRESSED; 770 event.handled = true; 771 } 772 773 void pointerReleased(WidgetProxy widget, ref PointerReleaseEvent event) 774 { 775 widget.get!State.data &= ~BUTTON_PRESSED; 776 event.handled = true; 777 } 778 779 void enterWidget(WidgetProxy widget, ref PointerEnterEvent event) 780 { 781 widget.get!State.data |= BUTTON_HOVERED; 782 } 783 784 void leaveWidget(WidgetProxy widget, ref PointerLeaveEvent event) 785 { 786 widget.get!State.data &= ~BUTTON_HOVERED; 787 } 788 } 789 790 alias CheckHandler = ref bool delegate(); 791 792 @Component("gui.BoolBinding", Replication.none) 793 struct BoolBinding 794 { 795 bool* value; 796 } 797 798 @Component("gui.UserCheckHandler", Replication.none) 799 struct UserCheckHandler 800 { 801 CheckHandler handler; 802 void onClick() { if (handler) toggle_bool(handler()); } 803 bool isChecked() { return handler ? handler() : false; } 804 } 805 806 alias ClickHandler = void delegate(); 807 808 @Component("gui.UserClickHandler", Replication.none) 809 struct UserClickHandler 810 { 811 void onClick() { if (handler) handler(); } 812 ClickHandler handler; 813 } 814 815 mixin template ButtonClickLogic(Data) 816 { 817 void clickWidget(WidgetProxy widget, ref PointerClickEvent event) 818 { 819 auto data = widget.get!Data; 820 data.onClick(); 821 } 822 823 void setHandler(WidgetProxy button, typeof(Data.handler) handler) { 824 auto data = button.get!Data; 825 auto events = button.get!WidgetEvents; 826 if (!data.handler) events.addEventHandler(&clickWidget); 827 data.handler = handler; 828 } 829 } 830 831 /// Widget will catch mouse events from bubbling 832 WidgetProxy consumeMouse(WidgetProxy widget) { widget.handlers(&handlePointerMoved); return widget; } 833 834 void handlePointerMoved(WidgetProxy widget, ref PointerMoveEvent event) { event.handled = true; } 835 836 WidgetProxy hline(WidgetProxy parent) { HLine.create(parent); return parent; } 837 WidgetProxy vline(WidgetProxy parent) { VLine.create(parent); return parent; } 838 839 alias HLine = Line!true; 840 alias VLine = Line!false; 841 842 struct Line(bool horizontal) 843 { 844 static: 845 static if (horizontal) { 846 WidgetProxy create(WidgetProxy parent) { 847 return parent.createChild(WidgetEvents(&drawWidget, &measure), WidgetType("HLine"), WidgetStyle(parent.ctx.style.color)).hexpand; 848 } 849 void measure(WidgetProxy widget, ref MeasureEvent event) { 850 auto transform = widget.getOrCreate!WidgetTransform; 851 transform.measuredSize = ivec2(0,1); 852 } 853 } else { 854 WidgetProxy create(WidgetProxy parent) { 855 return parent.createChild(WidgetEvents(&drawWidget, &measure), WidgetType("VLine"), WidgetStyle(parent.ctx.style.color)).vexpand; 856 } 857 void measure(WidgetProxy widget, ref MeasureEvent event) { 858 auto transform = widget.getOrCreate!WidgetTransform; 859 transform.measuredSize = ivec2(1,0); 860 } 861 } 862 void drawWidget(WidgetProxy widget, ref DrawEvent event) { 863 if (event.bubbling) return; 864 auto transform = widget.getOrCreate!WidgetTransform; 865 auto color = widget.get!WidgetStyle.color; 866 event.renderQueue.drawRectFill(vec2(transform.absPos), vec2(transform.size), event.depth, color); 867 } 868 } 869 870 WidgetProxy hfill(WidgetProxy parent) { HFill.create(parent); return parent; } 871 WidgetProxy vfill(WidgetProxy parent) { VFill.create(parent); return parent; } 872 873 alias HFill = Fill!true; 874 alias VFill = Fill!false; 875 876 struct Fill(bool horizontal) 877 { 878 static: 879 WidgetProxy create(WidgetProxy parent) 880 { 881 static if (horizontal) 882 return parent.createChild(WidgetType("Fill")).hexpand; 883 else 884 return parent.createChild(WidgetType("Fill")).vexpand; 885 } 886 } 887 888 WidgetProxy moveToTop(WidgetProxy widget) { 889 AutoMoveToTop.attachTo(widget); 890 return widget; 891 } 892 struct AutoMoveToTop 893 { 894 static void attachTo(WidgetProxy widget) 895 { 896 widget.handlers(&onPress); 897 } 898 static void onPress(WidgetProxy widget, ref PointerPressEvent event) 899 { 900 auto parentId = widget.get!WidgetTransform.parent; 901 if (parentId) 902 { 903 if (auto children = widget.ctx.get!WidgetContainer(parentId)) 904 { 905 children.bringToFront(widget.wid); 906 } 907 } 908 } 909 } 910 911 @Component("DraggableSettings", Replication.none) 912 struct DraggableSettings 913 { 914 PointerButton onButton; 915 } 916 917 WidgetProxy makeDraggable(WidgetProxy widget, PointerButton onButton = PointerButton.PB_1) { 918 DraggableLogic.attachTo(widget, onButton); return widget; } 919 920 struct DraggableLogic 921 { 922 static: 923 void attachTo(WidgetProxy widget, PointerButton onButton) 924 { 925 widget.handlers(&onPress, &onDrag).set(DraggableSettings(onButton)); 926 } 927 928 void onPress(WidgetProxy widget, ref PointerPressEvent event) 929 { 930 if (event.sinking) return; 931 if (event.button == widget.get!DraggableSettings.onButton) 932 { 933 event.handled = true; 934 event.beginDrag = true; 935 } 936 } 937 938 void onDrag(WidgetProxy widget, ref DragEvent event) 939 { 940 widget.get!WidgetTransform.relPos += event.delta; 941 } 942 } 943 944 struct ScrollableAreaParts 945 { 946 WidgetProxy scrollable; 947 alias scrollable this; 948 WidgetProxy canvas; 949 } 950 951 @Component("ScrollableData", Replication.none) 952 struct ScrollableData 953 { 954 ivec2 contentOffset; 955 ivec2 contentSize; 956 ivec2 windowSize; 957 bool isScrollbarNeeded() { return contentSize.y > windowSize.y; } 958 ivec2 maxPos() { return vector_max(ivec2(0,0), contentSize - windowSize); } 959 void clampPos() { 960 contentOffset = vector_clamp(contentOffset, ivec2(0,0), maxPos); 961 } 962 } 963 964 struct ScrollableArea 965 { 966 static: 967 ScrollableAreaParts create(WidgetProxy parent) { 968 auto scrollable = parent.createChild(WidgetType("Scrollable"), 969 ScrollableData(), 970 WidgetEvents(&onScroll, &measure, &layout, &onSliderDrag)).setHLayout(0, padding4(0)).measuredSize(10, 10); 971 auto container = scrollable.createChild(WidgetType("Container"), WidgetEvents(&clipDraw)).hvexpand; 972 auto canvas = container.createChild(WidgetType("Canvas")); 973 auto scrollbar = ScrollBarLogic.create(scrollable, scrollable/*receive drag event*/); 974 scrollbar.vexpand; 975 976 return ScrollableAreaParts(scrollable, canvas); 977 } 978 979 void onSliderDrag(WidgetProxy scrollable, ref ScrollBarEvent event) 980 { 981 auto data = scrollable.get!ScrollableData; 982 if (event.maxPos) 983 data.contentOffset.y = (event.pos * data.maxPos.y) / event.maxPos; 984 else 985 data.contentOffset.y = 0; 986 //data.clampPos; unnesessary 987 } 988 989 void onScroll(WidgetProxy scrollable, ref ScrollEvent event) 990 { 991 if (event.sinking) return; 992 auto data = scrollable.get!ScrollableData; 993 data.contentOffset += ivec2(event.delta) * 50; // TODO settings 994 data.clampPos; 995 event.handled = true; // TODO do not handle if not scrolled, so higher scrolls will work 996 } 997 998 void clipDraw(WidgetProxy scrollable, ref DrawEvent event) 999 { 1000 if (event.sinking) { 1001 auto transform = scrollable.getOrCreate!WidgetTransform; 1002 event.renderQueue.pushClipRect(irect(transform.absPos, transform.size)); 1003 } else { 1004 event.renderQueue.popClipRect(); 1005 } 1006 } 1007 1008 void measure(WidgetProxy scrollable, ref MeasureEvent event) 1009 { 1010 // scrollable -> container -> canvas 1011 auto canvasTransform = scrollable.children[0].children[0].get!WidgetTransform; 1012 scrollable.get!ScrollableData.contentSize = canvasTransform.measuredSize; 1013 } 1014 1015 void layout(WidgetProxy scrollable, ref LayoutEvent event) 1016 { 1017 auto data = scrollable.get!ScrollableData; 1018 auto rootTransform = scrollable.get!WidgetTransform; 1019 data.windowSize = rootTransform.size; 1020 data.clampPos; 1021 1022 WidgetProxy cont = scrollable.children[0]; 1023 auto contTran = cont.get!WidgetTransform; 1024 1025 auto containerTransform = cont.get!WidgetTransform; 1026 containerTransform.minSize = ivec2(0, 0); 1027 containerTransform.measuredSize = ivec2(0, 0); 1028 1029 auto canvasTransform = cont.children[0].get!WidgetTransform; 1030 auto scrollbar = scrollable.children[1]; 1031 1032 if (data.isScrollbarNeeded) 1033 { 1034 // set scrollbar 1035 scrollbar.remove!hidden; 1036 ScrollBarLogic.setPosSize(scrollable.children[1], data.contentOffset.y, data.contentSize.y, data.windowSize.y); 1037 canvasTransform.relPos.y = -data.contentOffset.y; 1038 } 1039 else 1040 { 1041 // no scrollbar 1042 scrollbar.set(hidden()); 1043 canvasTransform.relPos.y = 0; 1044 } 1045 } 1046 } 1047 1048 struct ScrollBarEvent 1049 { 1050 int pos; 1051 int maxPos; 1052 mixin GuiEvent!(); 1053 } 1054 1055 struct ScrollBarParts 1056 { 1057 WidgetProxy scrollbar; 1058 alias scrollbar this; 1059 WidgetProxy slider; 1060 } 1061 1062 struct ScrollBarLogic 1063 { 1064 static: 1065 ScrollBarParts create(WidgetProxy parent, WidgetId eventReceiver = WidgetId(0)) { 1066 auto scroll = parent.createChild(WidgetType("ScrollBar")) 1067 .vexpand.minSize(10, 20).addBackground(color_gray); 1068 auto slider = scroll.createChild(WidgetType("ScrollHandle"), WidgetReference(eventReceiver)) 1069 .minSize(10, 10) 1070 .measuredSize(0, 100) 1071 .handlers(&onSliderDrag, &DraggableLogic.onPress) 1072 .addBackground(color_asbestos); 1073 return ScrollBarParts(scroll, slider); 1074 } 1075 1076 void onSliderDrag(WidgetProxy slider, ref DragEvent event) 1077 { 1078 auto tr = slider.get!WidgetTransform; 1079 auto parent_tr = slider.ctx.get!WidgetTransform(tr.parent); 1080 int pos = slider.ctx.state.curPointerPos.y - slider.ctx.state.draggedWidgetOffset.y - parent_tr.absPos.y; 1081 int maxPos = parent_tr.size.y - tr.size.y; 1082 pos = clamp(pos, 0, maxPos); 1083 tr.relPos.y = pos; 1084 auto reference = slider.get!WidgetReference; 1085 if (reference.widgetId) 1086 slider.ctx.postEvent(reference.widgetId, ScrollBarEvent(pos, maxPos)); 1087 } 1088 1089 // Assumes clamped canvasPos 1090 void setPosSize(WidgetProxy scroll, int canvasPos, int canvasSize, int windowSize) 1091 { 1092 assert(canvasSize > windowSize); 1093 auto scrollTransform = scroll.get!WidgetTransform; 1094 auto sliderTransform = scroll.children[0].get!WidgetTransform; 1095 int scrollSize = windowSize; 1096 int sliderSize = (windowSize * scrollSize) / canvasSize; 1097 int sliderMaxPos = scrollSize - sliderSize; 1098 1099 sliderTransform.relPos.y = canvasPos * sliderMaxPos / (canvasSize - windowSize); 1100 sliderTransform.size.y = sliderSize; 1101 } 1102 } 1103 1104 @Component("gui.SingleLayoutSettings", Replication.none) 1105 struct SingleLayoutSettings 1106 { 1107 int padding; /// borders around items 1108 Alignment halign; 1109 Alignment valign; 1110 } 1111 1112 WidgetProxy setSingleLayout(WidgetProxy widget, int padding, Alignment halign = Alignment.center, Alignment valign = Alignment.center) 1113 { 1114 SingleLayout.attachTo(widget, padding, halign, valign); 1115 return widget; 1116 } 1117 1118 /// For layouting single child with alignment and padding. 1119 struct SingleLayout 1120 { 1121 static: 1122 void attachTo( 1123 WidgetProxy widget, 1124 int padding, 1125 Alignment halign = Alignment.center, 1126 Alignment valign = Alignment.center) 1127 { 1128 widget.set(SingleLayoutSettings(padding, halign, valign)); 1129 widget.handlers(&measure, &layout); 1130 } 1131 1132 void measure(WidgetProxy widget, ref MeasureEvent event) 1133 { 1134 auto settings = widget.get!SingleLayoutSettings; 1135 ivec2 childSize; 1136 1137 ChildrenRange children = widget.children; 1138 if (children.length > 0) 1139 { 1140 auto childTransform = children[0].get!WidgetTransform; 1141 childSize = childTransform.size; 1142 } 1143 1144 widget.get!WidgetTransform.measuredSize = childSize + settings.padding*2; 1145 } 1146 1147 void layout(WidgetProxy widget, ref LayoutEvent event) 1148 { 1149 auto settings = widget.get!SingleLayoutSettings; 1150 auto rootTransform = widget.get!WidgetTransform; 1151 1152 ivec2 childArea = rootTransform.size - settings.padding * 2; 1153 1154 ChildrenRange children = widget.children; 1155 if (children.length > 0) 1156 { 1157 WidgetProxy child = children[0]; 1158 auto childTransform = child.get!WidgetTransform; 1159 1160 ivec2 childSize; 1161 ivec2 relPos; 1162 1163 //widget.ctx.debugText.putfln("SLayout.layout %s tr %s", 1164 // widget.wid, *childTransform); 1165 1166 if (childTransform.hasVexpand) { 1167 childSize.x = childArea.x; 1168 relPos.x = settings.padding; 1169 } else { 1170 childSize.x = childTransform.measuredSize.x; 1171 relPos.x = settings.padding + alignOnAxis(childSize.x, settings.halign, childArea.x); 1172 } 1173 1174 if (childTransform.hasHexpand) { 1175 childSize.y = childArea.y; 1176 relPos.y = settings.padding; 1177 } else { 1178 childSize.y = childTransform.measuredSize.y; 1179 relPos.y = settings.padding + alignOnAxis(childSize.y, settings.valign, childArea.y); 1180 } 1181 1182 childTransform.relPos = relPos; 1183 childTransform.size = childSize; 1184 } 1185 } 1186 } 1187 1188 @Component("gui.LinearLayoutSettings", Replication.none) 1189 struct LinearLayoutSettings 1190 { 1191 int spacing; /// distance between items 1192 padding4 padding; /// borders around items 1193 Alignment alignment; 1194 1195 // internal state 1196 int numExpandableChildren; 1197 } 1198 1199 alias HLayout = LinearLayout!true; 1200 alias VLayout = LinearLayout!false; 1201 1202 alias setHLayout = setLinearLayout!true; 1203 alias setVLayout = setLinearLayout!false; 1204 1205 WidgetProxy setLinearLayout(bool hori)(WidgetProxy widget, int spacing, padding4 padding, Alignment alignTo = Alignment.min) 1206 { 1207 LinearLayout!hori.attachTo(widget, spacing, padding, alignTo); 1208 return widget; 1209 } 1210 1211 struct LinearLayout(bool horizontal) 1212 { 1213 static: 1214 WidgetProxy create(WidgetProxy parent, int spacing, padding4 padding, Alignment alignTo = Alignment.min) 1215 { 1216 WidgetProxy layout = parent.createChild(WidgetType("LinearLayout")); 1217 attachTo(layout, spacing, padding, alignTo); 1218 return layout; 1219 } 1220 1221 void attachTo(WidgetProxy widget, int spacing, padding4 padding, Alignment alignTo = Alignment.min) 1222 { 1223 //writefln("attachTo %s %s", widget.widgetType, widget.wid); 1224 widget.set(LinearLayoutSettings(spacing, padding, alignTo)); 1225 widget.getOrCreate!WidgetEvents.addEventHandlers(&measure, &layout); 1226 } 1227 1228 void measure(WidgetProxy widget, ref MeasureEvent event) 1229 { 1230 auto settings = widget.get!LinearLayoutSettings; 1231 settings.numExpandableChildren = 0; 1232 1233 int maxChildWidth = 0; 1234 int childrenLength; 1235 1236 ChildrenRange children = widget.children; 1237 foreach(child; children) 1238 { 1239 auto childTransform = child.get!WidgetTransform; 1240 childrenLength += length(childTransform.size); 1241 maxChildWidth = max(width(childTransform.size), maxChildWidth); 1242 if (hasExpandableLength(child)) ++settings.numExpandableChildren; 1243 } 1244 1245 static if (horizontal) int widthPad = settings.padding.vert; 1246 else int widthPad = settings.padding.hori; 1247 1248 static if (horizontal) int lengthPad = settings.padding.hori; 1249 else int lengthPad = settings.padding.vert; 1250 1251 int minRootWidth = maxChildWidth + widthPad; 1252 int minRootLength = childrenLength + cast(int)(children.length-1)*settings.spacing + lengthPad; 1253 auto transform = widget.get!WidgetTransform; 1254 transform.measuredSize = sizeFromWidthLength(minRootWidth, minRootLength); 1255 } 1256 1257 void layout(WidgetProxy widget, ref LayoutEvent event) 1258 { 1259 auto settings = widget.get!LinearLayoutSettings; 1260 auto rootTransform = widget.get!WidgetTransform; 1261 1262 static if (horizontal) int widthPad = settings.padding.vert; 1263 else int widthPad = settings.padding.hori; 1264 1265 int maxChildWidth = width(rootTransform.size) - widthPad; 1266 1267 int extraLength = length(rootTransform.size) - length(rootTransform.measuredSize); 1268 int extraPerWidget = settings.numExpandableChildren > 0 ? extraLength/settings.numExpandableChildren : 0; 1269 1270 static if (horizontal) int topOffset = settings.padding.left; 1271 else int topOffset = settings.padding.top; 1272 static if (horizontal) int widthOffset = settings.padding.top; 1273 else int widthOffset = settings.padding.left; 1274 1275 topOffset -= settings.spacing; // compensate extra spacing before first child 1276 1277 foreach(child; widget.children) 1278 { 1279 topOffset += settings.spacing; 1280 auto childTransform = child.get!WidgetTransform; 1281 childTransform.relPos = sizeFromWidthLength(widthOffset, topOffset); 1282 1283 ivec2 childSize = childTransform.constrainedSize; 1284 if (hasExpandableLength(child)) length(childSize) += extraPerWidget; 1285 if (hasExpandableWidth(child)) width(childSize) = maxChildWidth; 1286 else width(childTransform.relPos) += alignOnAxis(width(childSize), settings.alignment, maxChildWidth); 1287 childTransform.size = childSize; 1288 1289 //widget.ctx.debugText.putfln("LLayout.layout %s tr %s extra %s", 1290 // child.wid, *childTransform, extraPerWidget); 1291 1292 topOffset += length(childSize); 1293 } 1294 } 1295 1296 private: 1297 1298 bool hasExpandableWidth(WidgetProxy widget) { 1299 static if (horizontal) return widget.hasVexpand; 1300 else return widget.hasHexpand; 1301 } 1302 1303 bool hasExpandableLength(WidgetProxy widget) { 1304 static if (horizontal) return widget.hasHexpand; 1305 else return widget.hasVexpand; 1306 } 1307 1308 ivec2 sizeFromWidthLength(int width, int length) { 1309 static if (horizontal) return ivec2(length, width); 1310 else return ivec2(width, length); 1311 } 1312 1313 ref int length(return ref ivec2 vector) { 1314 static if (horizontal) return vector.x; 1315 else return vector.y; 1316 } 1317 1318 ref int width(ref ivec2 vector) { 1319 static if (horizontal) return vector.y; 1320 else return vector.x; 1321 } 1322 } 1323 1324 struct ColumnInfo 1325 { 1326 string name; 1327 int width = 100; 1328 Alignment alignment; 1329 enum int minWidth = 80; 1330 } 1331 1332 enum TreeLineType 1333 { 1334 leaf, 1335 collapsedNode, 1336 expandedNode 1337 } 1338 1339 abstract class ListModel 1340 { 1341 int numLines(); 1342 int numColumns(); 1343 ref ColumnInfo columnInfo(int column); 1344 void getColumnText(int column, scope void delegate(const(char)[]) sink); 1345 void getCellText(int column, int line, scope void delegate(const(char)[]) sink); 1346 bool isLineSelected(int line); 1347 void onLineClick(int line); 1348 TreeLineType getLineType(int line) { return TreeLineType.leaf; } 1349 int getLineIndent(int line) { return 0; } 1350 void toggleLineFolding(int line) { } 1351 } 1352 1353 1354 alias SinkT = void delegate(const(char)[]); 1355 alias Formatter(Row) = void function(Row row, scope SinkT sink); 1356 1357 struct Column(Row) 1358 { 1359 string name; 1360 int width; 1361 Formatter!Row formatter; 1362 } 1363 1364 struct ListInfo(Row) 1365 { 1366 ColumnInfo[] columnInfos; 1367 void function(Row row, scope SinkT sink)[] formatters; 1368 } 1369 1370 ListInfo!Row parseListInfo(Row)() 1371 { 1372 import std.traits; 1373 ListInfo!Row result; 1374 Row r; 1375 foreach(string memberName; __traits(allMembers, Row)) 1376 { 1377 foreach(attr; __traits(getAttributes, __traits(getMember, r, memberName))) 1378 { 1379 static if (is(typeof(attr) == Column!Row)) 1380 { 1381 result.columnInfos ~= ColumnInfo(attr.name, attr.width); 1382 result.formatters ~= attr.formatter; 1383 } 1384 } 1385 } 1386 return result; 1387 } 1388 1389 class AutoListModel(Model) : ListModel 1390 { 1391 alias Row = typeof(Model.init[0]); 1392 Model model; 1393 ListInfo!Row info = parseListInfo!Row; 1394 int selectedRow = -1; 1395 1396 this(Model)(Model model) 1397 { 1398 this.model = model; 1399 } 1400 1401 override int numLines() { return cast(int)model.length; } 1402 override int numColumns() { return cast(int)info.columnInfos.length; } 1403 override ref ColumnInfo columnInfo(int column) { 1404 return info.columnInfos[column]; 1405 } 1406 override void getColumnText(int column, scope SinkT sink) { 1407 sink(info.columnInfos[column].name); 1408 } 1409 override void getCellText(int column, int row, scope SinkT sink) { 1410 info.formatters[column](model[row], sink); 1411 } 1412 override bool isLineSelected(int row) { 1413 return row == selectedRow; 1414 } 1415 override void onLineClick(int row) { 1416 selectedRow = row; 1417 } 1418 1419 bool hasSelected() @property { 1420 return selectedRow < model.length && selectedRow >= 0; 1421 } 1422 } 1423 1424 class ArrayListModel(Row) : AutoListModel!(Row[]) 1425 { 1426 this(Row[] rows) { super(rows); } 1427 } 1428 1429 @Component("gui.ListData", Replication.none) 1430 struct ListData 1431 { 1432 ListModel model; 1433 FontRef font; 1434 ivec2 headerPadding = ivec2(5, 5); 1435 ivec2 contentPadding = ivec2(5, 2); 1436 enum scrollSpeedLines = 1; 1437 int hoveredLine = -1; 1438 ivec2 viewOffset; // on canvas 1439 1440 bool hasHoveredLine() { return hoveredLine >= 0 && hoveredLine < model.numLines; } 1441 int textHeight() { return font.metrics.height; } 1442 int lineHeight() { return textHeight + contentPadding.y*2; } 1443 int headerHeight() { return textHeight + headerPadding.y*2; } 1444 int canvasHeight() { return lineHeight * model.numLines; } 1445 } 1446 1447 struct ColumnListLogic 1448 { 1449 static: 1450 WidgetProxy create(WidgetProxy parent, ListModel model) 1451 { 1452 WidgetProxy list = parent.createChild( 1453 ListData(model, parent.ctx.style.font), 1454 WidgetEvents( 1455 &drawWidget, &pointerMoved, &pointerPressed, &pointerReleased, 1456 &enterWidget, &leaveWidget, &clickWidget, &onScroll), 1457 WidgetStyle(baseColor), 1458 WidgetType("List")); 1459 return list; 1460 } 1461 1462 void drawWidget(WidgetProxy widget, ref DrawEvent event) 1463 { 1464 if (event.bubbling) return; 1465 1466 auto data = widget.get!ListData; 1467 auto transform = widget.getOrCreate!WidgetTransform; 1468 auto style = widget.get!WidgetStyle; 1469 1470 irect transformRect = irect(transform.absPos, transform.size); 1471 1472 int numLines = data.model.numLines; 1473 int numColumns = data.model.numColumns; 1474 1475 int textHeight = data.textHeight; 1476 int lineHeight = data.lineHeight; 1477 int headerHeight = data.headerHeight; 1478 int canvasHeight = lineHeight * data.model.numLines; 1479 1480 // calc content size in pixels 1481 ivec2 canvasSize; 1482 canvasSize.y = lineHeight * numLines; 1483 foreach(column; 0..numColumns) 1484 canvasSize.x += data.model.columnInfo(column).width; 1485 int lastLine = numLines ? numLines-1 : 0; 1486 1487 // calc visible lines 1488 data.viewOffset = vector_clamp(data.viewOffset, ivec2(0, 0), canvasSize); 1489 int firstVisibleLine = clamp(data.viewOffset.y / lineHeight, 0, lastLine); 1490 int viewEndPos = data.viewOffset.y + canvasSize.y; 1491 int lastVisibleLine = clamp(viewEndPos / lineHeight, 0, lastLine); 1492 1493 int numVisibleLines = clamp(lastVisibleLine - firstVisibleLine + 1, 0, numLines); 1494 1495 // for folding arrow positioning 1496 int charW = data.font.metrics.advanceX; 1497 1498 bool isLineHovered(int line) { return data.hoveredLine == line; } 1499 1500 void drawBackground() 1501 { 1502 int lineY = transform.absPos.y + headerHeight; 1503 foreach(visibleLine; 0..numVisibleLines) 1504 { 1505 int line = firstVisibleLine + visibleLine; 1506 auto color_selected = rgb(217, 235, 249); 1507 auto color_hovered = rgb(207, 225, 239); 1508 1509 Color4ub color; 1510 if (isLineHovered(line)) color = color_hovered; 1511 else if (data.model.isLineSelected(line)) color = color_selected; 1512 else color = line % 2 ? rgb(255, 255, 255) : rgb(250, 250, 250); // color_white 1513 1514 event.renderQueue.drawRectFill( 1515 vec2(transform.absPos.x, lineY), 1516 vec2(transform.size.x, lineHeight), 1517 event.depth, color); 1518 lineY += lineHeight; 1519 } 1520 } 1521 1522 void drawColumnHeader(int column, ivec2 pos, ivec2 size) 1523 { 1524 auto params = event.renderQueue.startTextAt(vec2(pos)); 1525 params.font = data.font; 1526 params.color = color_wet_asphalt; 1527 params.depth = event.depth+2; 1528 //params.monospaced = true; 1529 params.scissors = rectIntersection(irect(pos, size), transformRect); 1530 //params.scissors = irect(pos, size); 1531 params.meshText(data.model.columnInfo(column).name); 1532 } 1533 1534 void drawCell(int column, int line, irect rect) 1535 { 1536 auto params = event.renderQueue.startTextAt(vec2(rect.position)); 1537 params.font = data.font; 1538 params.color = color_wet_asphalt; 1539 params.depth = event.depth+2; 1540 //params.monospaced = true; 1541 params.scissors = rectIntersection(rect, transformRect); 1542 1543 void sinkHandler(const(char)[] str) { 1544 params.meshText(str); 1545 } 1546 1547 if (column == 0) 1548 { 1549 params.origin.x += charW * data.model.getLineIndent(line); 1550 final switch(data.model.getLineType(line)) 1551 { 1552 case TreeLineType.leaf: params.meshText(" "); break; 1553 case TreeLineType.collapsedNode: params.meshText("► "); break; 1554 case TreeLineType.expandedNode: params.meshText("▼ "); break; 1555 } 1556 } 1557 1558 data.model.getCellText(column, line, &sinkHandler); 1559 params.alignMeshedText(data.model.columnInfo(column).alignment, Alignment.min, rect.size); 1560 } 1561 1562 event.renderQueue.pushClipRect(transformRect); 1563 drawBackground(); 1564 1565 int colX = transform.absPos.x; 1566 // columns 1567 foreach(column; 0..data.model.numColumns) 1568 { 1569 int colW = data.model.columnInfo(column).width; 1570 int cellW = colW - data.contentPadding.x*2; 1571 1572 // separator 1573 ivec2 separatorStart = ivec2(colX + colW-1, transform.absPos.y); 1574 event.renderQueue.drawRectFill(vec2(separatorStart), vec2(1, transform.size.y), event.depth+3, color_silver); 1575 1576 // clip 1577 event.renderQueue.pushClipRect(irect(colX+data.contentPadding.x, transform.absPos.y, cellW, transform.size.y)); 1578 1579 // header 1580 ivec2 headerPos = ivec2(colX, transform.absPos.y) + data.headerPadding; 1581 ivec2 headerSize = ivec2(colW, headerHeight) - data.headerPadding*2; 1582 drawColumnHeader(column, headerPos, headerSize); 1583 int lineY = transform.absPos.y + headerHeight; 1584 1585 // cells 1586 foreach(line; 0..numVisibleLines) 1587 { 1588 ivec2 cellPos = ivec2(colX, lineY); 1589 ivec2 cellSize = ivec2(colW, lineHeight); 1590 1591 ivec2 cellContentPos = cellPos + data.contentPadding; 1592 ivec2 cellContentSize = cellSize - data.contentPadding*2; 1593 1594 drawCell(column, firstVisibleLine+line, irect(cellContentPos, cellContentSize)); 1595 lineY += lineHeight; 1596 } 1597 1598 event.renderQueue.popClipRect(); 1599 colX += colW; 1600 } 1601 event.renderQueue.popClipRect(); 1602 1603 event.depth += 3; 1604 } 1605 1606 void updateHoveredLine(WidgetProxy widget, ivec2 pointerPos) 1607 { 1608 auto transform = widget.getOrCreate!WidgetTransform; 1609 auto data = widget.get!ListData; 1610 int localPointerY = pointerPos.y - transform.absPos.y; 1611 int viewY = localPointerY - data.headerHeight; 1612 double canvasY = viewY + data.viewOffset.y; 1613 data.hoveredLine = cast(int)floor(canvasY / data.lineHeight); 1614 if (data.hoveredLine < 0 || data.hoveredLine >= data.model.numLines) 1615 data.hoveredLine = -1; 1616 } 1617 1618 void onScroll(WidgetProxy widget, ref ScrollEvent event) 1619 { 1620 auto data = widget.get!ListData; 1621 data.viewOffset += ivec2(event.delta * data.scrollSpeedLines * data.lineHeight); 1622 } 1623 1624 void pointerMoved(WidgetProxy widget, ref PointerMoveEvent event) 1625 { 1626 updateHoveredLine(widget, event.newPointerPos); 1627 event.handled = true; 1628 } 1629 1630 void pointerPressed(WidgetProxy widget, ref PointerPressEvent event) 1631 { 1632 event.handled = true; 1633 } 1634 1635 void pointerReleased(WidgetProxy widget, ref PointerReleaseEvent event) 1636 { 1637 event.handled = true; 1638 } 1639 1640 void enterWidget(WidgetProxy widget, ref PointerEnterEvent event) 1641 { 1642 widget.get!ListData.hoveredLine = -1; 1643 } 1644 1645 void leaveWidget(WidgetProxy widget, ref PointerLeaveEvent event) 1646 { 1647 widget.get!ListData.hoveredLine = -1; 1648 } 1649 1650 void clickWidget(WidgetProxy widget, ref PointerClickEvent event) 1651 { 1652 auto data = widget.get!ListData; 1653 if (data.hasHoveredLine) 1654 { 1655 auto line = data.hoveredLine; 1656 if (data.model.numColumns < 1) return; 1657 1658 auto lineType = data.model.getLineType(line); 1659 if (lineType == TreeLineType.leaf) 1660 { 1661 data.model.onLineClick(line); 1662 return; 1663 } 1664 1665 int firstColW = data.model.columnInfo(0).width; 1666 auto transform = widget.getOrCreate!WidgetTransform; 1667 auto leftBorder = transform.absPos.x + data.contentPadding.x; 1668 auto indentW = data.font.metrics.advanceX; 1669 auto buttonStart = leftBorder + indentW * data.model.getLineIndent(line); 1670 auto buttonW = indentW*3; 1671 auto buttonEnd = buttonStart + buttonW; 1672 auto clickX = event.pointerPosition.x; 1673 1674 if (clickX >= buttonStart && clickX < buttonEnd) 1675 { 1676 data.model.toggleLineFolding(line); 1677 } 1678 else 1679 data.model.onLineClick(line); 1680 } 1681 else 1682 { 1683 data.model.onLineClick(-1); 1684 } 1685 } 1686 } 1687 1688 /// Creates a frame that shows a tree of all widgets. 1689 /// Highlights clicked widgets 1690 WidgetProxy createGuiDebugger(WidgetProxy root) 1691 { 1692 WidgetId highlightedWidget; 1693 1694 // Tree widget 1695 struct TreeNode 1696 { 1697 WidgetId wid; 1698 TreeLineType nodeType; 1699 int indent; 1700 int numExpandedChildren; 1701 } 1702 1703 class WidgetTreeModel : ListModel 1704 { 1705 import std.format : formattedWrite; 1706 import voxelman.container.gapbuffer; 1707 GapBuffer!TreeNode nodeList; 1708 WidgetProxy widgetAt(size_t i) { return WidgetProxy(nodeList[i].wid, root.ctx); } 1709 int selectedLine = -1; 1710 ColumnInfo[2] columnInfos = [ColumnInfo("Type", 200), ColumnInfo("Id", 50, Alignment.max)]; 1711 1712 void clear() 1713 { 1714 nodeList.clear(); 1715 selectedLine = -1; 1716 } 1717 1718 override int numLines() { return cast(int)nodeList.length; } 1719 override int numColumns() { return 2; } 1720 override ref ColumnInfo columnInfo(int column) { 1721 return columnInfos[column]; 1722 } 1723 override void getColumnText(int column, scope void delegate(const(char)[]) sink) { 1724 if (column == 0) sink("Widget type"); 1725 else if (column == 1) sink("Widget id"); 1726 else assert(false); 1727 } 1728 override void getCellText(int column, int line, scope void delegate(const(char)[]) sink) { 1729 if (column == 0) sink(widgetAt(line).widgetType); 1730 else formattedWrite(sink, "%s", nodeList[line].wid); 1731 } 1732 override bool isLineSelected(int line) { return line == selectedLine; } 1733 override void onLineClick(int line) { 1734 selectedLine = line; 1735 if (selectedLine == -1) 1736 highlightedWidget = 0; 1737 else 1738 highlightedWidget = nodeList[line].wid; 1739 } 1740 override TreeLineType getLineType(int line) { 1741 return nodeList[line].nodeType; 1742 } 1743 override int getLineIndent(int line) { return nodeList[line].indent; } 1744 override void toggleLineFolding(int line) { 1745 if (nodeList[line].nodeType == TreeLineType.collapsedNode) expandWidget(line); 1746 else collapseWidget(line); 1747 } 1748 void expandWidget(int line) 1749 { 1750 //writefln("expand %s %s", line, nodeList[line].wid); 1751 auto container = root.ctx.get!WidgetContainer(nodeList[line].wid); 1752 if (container is null || container.children.length == 0) { 1753 nodeList[line].nodeType = TreeLineType.leaf; 1754 return; 1755 } 1756 auto insertPos = line+1; 1757 auto indent = nodeList[line].indent+1; 1758 foreach(wid; container.children) 1759 { 1760 TreeLineType nodeType = numberOfChildren(root.ctx, wid) ? TreeLineType.collapsedNode : TreeLineType.leaf; 1761 nodeList.putAt(insertPos++, TreeNode(wid, nodeType, indent)); 1762 } 1763 nodeList[line].nodeType = TreeLineType.expandedNode; 1764 } 1765 void collapseWidget(int line) 1766 { 1767 //writefln("collapse %s", line, nodeList[line].wid); 1768 if (line+1 == nodeList.length) { 1769 nodeList[line].nodeType = TreeLineType.leaf; 1770 return; 1771 } 1772 1773 auto parentIndent = nodeList[line].indent; 1774 size_t numItemsToRemove; 1775 foreach(node; nodeList[line+1..$]) 1776 { 1777 if (node.indent <= parentIndent) break; 1778 ++numItemsToRemove; 1779 } 1780 nodeList.remove(line+1, numItemsToRemove); 1781 nodeList[line].nodeType = TreeLineType.collapsedNode; 1782 } 1783 } 1784 1785 void drawHighlight(WidgetProxy widget, ref DrawEvent event) 1786 { 1787 if (event.bubbling) return; 1788 1789 // highlight widget 1790 auto t = widget.ctx.get!WidgetTransform(highlightedWidget); 1791 if (t) 1792 { 1793 event.renderQueue.pushClipRect(irect(ivec2(0,0), event.renderQueue.renderer.framebufferSize)); 1794 event.renderQueue.drawRectLine(vec2(t.absPos), vec2(t.size), 10_000, Colors.red); 1795 event.renderQueue.popClipRect(); 1796 } 1797 } 1798 1799 auto model = new WidgetTreeModel; 1800 auto tree_frame = Frame.create(root); 1801 tree_frame.getOrCreate!WidgetEvents.addEventHandler(&drawHighlight); 1802 tree_frame.minSize(250, 400).pos(10, 10).makeDraggable.moveToTop; 1803 tree_frame.container.setVLayout(2, padding4(2)); 1804 tree_frame.header.setHLayout(2, padding4(4), Alignment.center); 1805 tree_frame.header.createIcon("tree", ivec2(16, 16), Colors.black); 1806 tree_frame.header.createText("Widget tree"); 1807 auto widget_tree = ColumnListLogic.create(tree_frame.container, model).minSize(250, 300).hvexpand; 1808 1809 void refillTree() 1810 { 1811 model.clear; 1812 foreach(rootId; root.ctx.roots) 1813 { 1814 TreeLineType nodeType = numberOfChildren(root.ctx, rootId) ? TreeLineType.collapsedNode : TreeLineType.leaf; 1815 model.nodeList.put(TreeNode(rootId, nodeType)); 1816 } 1817 } 1818 refillTree(); 1819 createTextButton(tree_frame.container, "Refresh", &refillTree).hexpand; 1820 1821 return tree_frame; 1822 }