1 /// This module defines templates and structs used to build themes, including a set of special setters to use within 2 /// theme definitions. Because of the amount of general symbols defined by the module, it is not imported by default 3 /// and has to be imported explicitly. Do not import this globally, but within functions that define themes. 4 module fluid.theme; 5 6 import std.meta; 7 import std.range; 8 import std.string; 9 import std.traits; 10 import std.exception; 11 12 import fluid.node; 13 import fluid.style; 14 import fluid.backend; 15 import fluid.structs; 16 17 18 @safe: 19 20 21 deprecated("Styles have been reworked and defineStyles is now a no-op. To be removed in 0.8.0.") { 22 mixin template defineStyles(args...) { } 23 mixin template DefineStyles(args...) { } 24 } 25 26 deprecated("makeTheme is now a no-op. Use `Theme()` and refer to the changelog for updates. To be removed in 0.8.0.") 27 Theme makeTheme(string s, Ts...)(Ts) { 28 29 return Theme.init; 30 31 } 32 33 /// Node theme. 34 struct Theme { 35 36 Rule[][TypeInfo_Class] rules; 37 38 /// Create a new theme using the given rules. 39 this(Rule[] rules...) { 40 41 // Inherit from default theme 42 this(fluidDefaultTheme.rules.dup); 43 add(rules); 44 45 } 46 47 /// Create a theme using the given set of rules. 48 this(Rule[][TypeInfo_Class] rules) { 49 50 this.rules = rules; 51 52 } 53 54 /// Check if the theme was initialized. 55 bool opCast(T : bool)() const { 56 57 return rules !is null; 58 59 } 60 61 /// Create a new theme that derives from another. 62 /// 63 /// Note: This doesn't duplicate rules. If rules are changed or reassigned, they will may the parent theme. In a 64 /// typical scenario you only add new rules. 65 Theme derive(Rule[] rules...) { 66 67 auto newTheme = this.dup; 68 newTheme.add(rules); 69 return newTheme; 70 71 } 72 73 /// Add rules to the theme. 74 void add(Rule[] rules...) { 75 76 foreach (rule; rules) { 77 78 this.rules[rule.selector.type] ~= rule; 79 80 } 81 82 } 83 84 /// Make the node use this theme. 85 void apply(Node node) { 86 87 node.theme = this; 88 89 } 90 91 /// Apply this theme on the given style. 92 /// Returns: An array of delegates used to update the style at runtime. 93 Rule.StyleDelegate[] apply(Node node, ref Style style) { 94 95 Rule.StyleDelegate[] dgs; 96 97 void applyFor(TypeInfo_Class ti) { 98 99 // Inherit from parents 100 if (ti.base) applyFor(ti.base); 101 102 // Find matching rules 103 if (auto rules = ti in rules) { 104 105 // Test against every rule 106 foreach (rule; *rules) { 107 108 // Run the rule, and add the callback if any 109 if (rule.applyStatic(node, style) && rule.styleDelegate) { 110 111 dgs ~= rule.styleDelegate; 112 113 } 114 115 } 116 117 } 118 119 } 120 121 applyFor(typeid(node)); 122 123 return dgs; 124 125 } 126 127 /// Duplicate the theme. This is not recursive; rules are not copied. 128 Theme dup() { 129 130 return Theme(rules.dup); 131 132 } 133 134 } 135 136 unittest { 137 138 import fluid.label; 139 140 Theme theme; 141 142 with (Rule) 143 theme.add( 144 rule!Label( 145 textColor = color!"#abc", 146 ), 147 ); 148 149 auto io = new HeadlessBackend; 150 auto root = label(theme, "placeholder"); 151 root.io = io; 152 153 root.draw(); 154 io.assertTexture(root.text.texture.chunks[0], Vector2(0, 0), color!"#fff"); 155 assert(root.text.texture.chunks[0].palette[0] == color("#abc")); 156 157 } 158 159 unittest { 160 161 import fluid.frame; 162 163 auto frameRule = rule!Frame( 164 Rule.margin.sideX = 8, 165 Rule.margin.sideY = 4, 166 ); 167 auto theme = nullTheme.derive(frameRule); 168 169 // Test selector 170 assert(frameRule.selector.type == typeid(Frame)); 171 assert(frameRule.selector.tags.empty); 172 173 // Test fields 174 auto style = Style.init; 175 frameRule.apply(vframe(), style); 176 assert(style.margin == [8, 8, 4, 4]); 177 assert(style.padding == style.init.padding); 178 assert(style.textColor == style.textColor.init); 179 180 // No dynamic rules 181 assert(rule.styleDelegate is null); 182 183 auto io = new HeadlessBackend; 184 auto root = vframe(theme); 185 root.io = io; 186 187 root.draw(); 188 189 assert(root.style == style); 190 191 } 192 193 unittest { 194 195 // Inheritance 196 197 import fluid.label; 198 import fluid.button; 199 200 auto myTheme = nullTheme.derive( 201 rule!Node( 202 Rule.margin.sideX = 8, 203 ), 204 rule!Label( 205 Rule.margin.sideTop = 6, 206 ), 207 rule!Button( 208 Rule.margin.sideBottom = 4, 209 ), 210 ); 211 212 auto style = Style.init; 213 myTheme.apply(button("", delegate { }), style); 214 215 assert(style.margin == [8, 8, 6, 4]); 216 217 } 218 219 unittest { 220 221 // Copying rules & dynamic rules 222 223 import fluid.label; 224 import fluid.button; 225 226 auto myRule = rule!Label( 227 Rule.textColor = color!"011", 228 Rule.backgroundColor = color!"faf", 229 (Label node) => node.isDisabled 230 ? rule(Rule.tint = color!"000a") 231 : rule() 232 ); 233 234 auto myTheme = Theme( 235 rule!Label( 236 myRule, 237 ), 238 rule!Button( 239 myRule, 240 Rule.textColor = color!"012", 241 ), 242 ); 243 244 auto style = Style.init; 245 auto myLabel = label(""); 246 247 // Apply the style, including dynamic rules 248 auto cbs = myTheme.apply(myLabel, style); 249 assert(cbs.length == 1); 250 cbs[0](myLabel).apply(myLabel, style); 251 252 assert(style.textColor == color!"011"); 253 assert(style.backgroundColor == color!"faf"); 254 assert(style.tint == Style.init.tint); 255 256 // Disable the node and apply again, it should change nothing 257 myLabel.disable(); 258 myTheme.apply(myLabel, style); 259 assert(style.tint == Style.init.tint); 260 261 // Apply the callback, tint should change 262 cbs[0](myLabel).apply(myLabel, style); 263 assert(style.tint == color!"000a"); 264 265 } 266 267 unittest { 268 269 import fluid.label; 270 271 auto myLabel = label(""); 272 273 void testMargin(Rule rule, float[4] margin) { 274 275 auto style = Style.init; 276 277 rule.apply(myLabel, style); 278 279 assert(style.margin == margin); 280 281 } 282 283 with (Rule) { 284 285 testMargin(rule(margin = 2), [2, 2, 2, 2]); 286 testMargin(rule(margin.sideX = 2), [2, 2, 0, 0]); 287 testMargin(rule(margin.sideY = 2), [0, 0, 2, 2]); 288 testMargin(rule(margin.sideTop = 2), [0, 0, 2, 0]); 289 testMargin(rule(margin.sideBottom = 2), [0, 0, 0, 2]); 290 testMargin(rule(margin.sideX = 2, margin.sideY = 4), [2, 2, 4, 4]); 291 testMargin(rule(margin = [1, 2, 3, 4]), [1, 2, 3, 4]); 292 testMargin(rule(margin.sideX = [1, 2]), [1, 2, 0, 0]); 293 294 } 295 296 } 297 298 unittest { 299 300 import std.math; 301 import fluid.label; 302 303 auto myRule = rule( 304 Rule.opacity = 0.5, 305 ); 306 auto style = Style.init; 307 308 myRule.apply(label(""), style); 309 310 assert(style.opacity.isClose(127/255f)); 311 assert(style.tint == color!"ffffff7f"); 312 313 auto secondRule = rule( 314 Rule.tint = color!"abc", 315 Rule.opacity = 0.6, 316 ); 317 318 style = Style.init; 319 secondRule.apply(label(""), style); 320 321 assert(style.opacity.isClose(153/255f)); 322 assert(style.tint == color!"abc9"); 323 324 } 325 326 // Define field setters for each field, to use by importing or through `Rule.property = value` 327 static foreach (field; StyleTemplate.fields) { 328 329 mixin( 330 `alias `, 331 __traits(identifier, field), 332 `= Field!("`, 333 __traits(identifier, field), 334 `", typeof(field)).make;` 335 ); 336 337 } 338 339 // Separately define opacity for convenience 340 auto opacity(float value) { 341 342 import std.algorithm : clamp; 343 344 return tint.a = cast(ubyte) clamp(value * ubyte.max, ubyte.min, ubyte.max); 345 346 } 347 348 /// Selector is used to pick a node based on its type and specified tags. 349 struct Selector { 350 351 /// Type of the node to match. 352 TypeInfo_Class type; 353 354 /// Tags needed by the selector. 355 TagList tags; 356 357 /// If true, this selector will reject any match. 358 bool rejectAll; 359 360 /// Returns a selector that doesn't match anything 361 static Selector none() { 362 363 Selector result; 364 result.rejectAll = true; 365 return result; 366 367 } 368 369 /// Test if the selector matches given node. 370 bool test(Node node) { 371 372 return !rejectAll 373 && testType(typeid(node)) 374 && testTags(node.tags); 375 376 } 377 378 /// Test if the given type matches the selector. 379 bool testType(TypeInfo_Class type) { 380 381 return !this.type || this.type.isBaseOf(type); 382 383 } 384 385 unittest { 386 387 import fluid.input; 388 import fluid.label; 389 import fluid.button; 390 391 auto myLabel = label("my label"); 392 auto myButton = button("my button", delegate { }); 393 394 auto selector = Selector(typeid(Node)); 395 396 assert(selector.test(myLabel)); 397 assert(selector.test(myButton)); 398 399 auto anotherSelector = Selector(typeid(Button)); 400 401 assert(!anotherSelector.test(myLabel)); 402 assert(anotherSelector.test(myButton)); 403 404 auto noSelector = Selector(); 405 406 assert(noSelector.test(myLabel)); 407 assert(noSelector.test(myButton)); 408 409 } 410 411 /// True if all tags in this selector are present on the given node. 412 bool testTags(TagList tags) { 413 414 // TODO linear search if there's only one tag 415 return this.tags.intersect(tags).walkLength == this.tags.length; 416 417 } 418 419 unittest { 420 421 import fluid.label; 422 423 @NodeTag enum good; 424 @NodeTag enum bad; 425 426 auto selector = Selector(typeid(Label)).addTags!good; 427 428 assert(selector.test(label(.tags!good, ""))); 429 assert(!selector.test(label(.tags!bad, ""))); 430 assert(!selector.test(label(""))); 431 432 } 433 434 /// Create a new selector requiring the given set of tags. 435 Selector addTags(tags...)() { 436 437 return Selector(type, this.tags.add!tags); 438 439 } 440 441 } 442 443 /// Create a style rule for the given node. 444 /// 445 /// Template parameters are used to select the node the rule applies to, based on its type and the tags it has. Regular 446 /// parameters define the changes made by the rule. These are created by using automatically defined members of `Rule`, 447 /// which match names of `Style` fields. For example, to change the property `Style.textColor`, one would assign 448 /// `Rule.textColor` inside this parameter list. 449 /// 450 /// --- 451 /// rule!Label( 452 /// Rule.textColor = color("#fff"), 453 /// Rule.backgroundColor = color("#000"), 454 /// ) 455 /// --- 456 /// 457 /// It is also possible to pass a `when` subrule to apply changes based on runtime conditions: 458 /// 459 /// --- 460 /// rule!Button( 461 /// Rule.backgroundColor = color("#fff"), 462 /// when!"a.isHovered"( 463 /// Rule.backgroundColor = color("#ccc"), 464 /// ) 465 /// ) 466 /// --- 467 /// 468 /// If some directives are repeated across different rules, they can be reused: 469 /// 470 /// --- 471 /// myRule = rule( 472 /// Rule.textColor = color("#000"), 473 /// ), 474 /// rule!Button( 475 /// myRule, 476 /// Rule.backgroundColor = color("#fff"), 477 /// ) 478 /// --- 479 /// 480 /// Moreover, rules respect inheritance. Since `Button` derives from `Label` and `Node`, `rule!Label` will also apply 481 /// to buttons, and so will `rule!Node`. 482 /// 483 /// For more advanced use-cases, it is possible to directly pass a delegate that accepts a node and returns a 484 /// subrule. 485 /// 486 /// --- 487 /// rule!Button( 488 /// a => rule( 489 /// Rule.backgroundColor = pickColor(), 490 /// ) 491 /// ) 492 /// --- 493 /// 494 /// It is recommended to use the `with (Rule)` statement to make rule definitions clearer. 495 template rule(T : Node = Node, tags...) { 496 497 Rule rule(Ts...)(Ts fields) { 498 499 enum isWhenRule(alias field) = is(typeof(field) : WhenRule!dg, alias dg); 500 enum isDynamicRule(alias field) = isCallable!field || isWhenRule!field || is(typeof(field) : Rule); 501 502 Rule result; 503 Rule[] crumbs; 504 505 // Create the selector 506 result.selector = Selector(typeid(T)).addTags!tags; 507 508 // Load fields 509 static foreach (i, field; fields) {{ 510 511 // Directly assigned field 512 static if (is(typeof(field) : Field!(fieldName, T), string fieldName, T)) { 513 514 // Add to the result 515 field.apply(result.fields); 516 517 } 518 519 // Copy from another rule 520 else static if (is(typeof(field) : Rule)) { 521 522 assert(field.selector.testType(typeid(T)), 523 format!"Cannot paste rule for %s into a rule for %s"(field.selector.type, typeid(T))); 524 525 // Copy fields 526 field.fields.apply(result.fields); 527 528 // Merge breadcrumbs 529 result.breadcrumbs.crumbs ~= field.breadcrumbs.crumbs; 530 531 // Also add delegates below... 532 533 } 534 535 // Children rule 536 else static if (is(typeof(field) : ChildrenRule)) { 537 538 // Insert the rule into breadcrumbs 539 crumbs ~= field.rule; 540 541 } 542 543 // Dynamic rule 544 else static if (isDynamicRule!field) { } 545 546 else static assert(false, format!"Unrecognized type %s (argument index %s)"(typeof(field).stringof, i)); 547 548 }} 549 550 // Load delegates 551 alias delegates = Filter!(isDynamicRule, fields); 552 553 // Build the dynamic rule delegate 554 static if (delegates.length) 555 result.styleDelegate = (Node node) { 556 557 Rule dynamicResult; 558 559 // Cast the node into proper type 560 auto castNode = cast(T) node; 561 assert(castNode, "styleDelegate was passed an invalid node"); 562 563 static foreach (dg; delegates) { 564 565 // A "when" rule 566 static if (isWhenRule!dg) { 567 568 // Test the predicate before applying the result 569 if (dg.predicate(castNode)) { 570 571 dg.rule.apply(node, dynamicResult); 572 573 } 574 575 // Apply the alternative if predicate fails 576 else dg.alternativeRule.apply(node, dynamicResult); 577 578 } 579 580 // Regular rule, forward to its delegate 581 else static if (is(typeof(dg) : Rule)) { 582 583 if (dg.styleDelegate) 584 dg.styleDelegate(node).apply(node, dynamicResult); 585 586 } 587 588 // Use the delegate and apply the result on the template of choice 589 else dg(castNode).apply(node, dynamicResult); 590 591 } 592 593 return dynamicResult; 594 595 }; 596 597 // Append ruleset from breadcrumbs to current breadcrumbs 598 if (crumbs) { 599 result.breadcrumbs.crumbs ~= crumbs; 600 } 601 602 return result; 603 604 } 605 606 } 607 608 @trusted 609 unittest { 610 611 // Rule copying semantics 612 613 import fluid.label; 614 import fluid.button; 615 import core.exception : AssertError; 616 617 auto generalRule = rule( 618 Rule.textColor = color!"#001", 619 ); 620 auto buttonRule = rule!Button( 621 Rule.backgroundColor = color!"#002", 622 generalRule, 623 ); 624 assertThrown!AssertError( 625 rule!Label(buttonRule), 626 "Label rule cannot inherit from a Button rule." 627 ); 628 629 assertNotThrown(rule!Button(buttonRule)); 630 assertNotThrown(rule!Button(rule!Label())); 631 632 } 633 634 @("Dynamic rules cannot inherit from mismatched rules") 635 unittest { 636 637 import fluid.space; 638 import fluid.frame; 639 640 auto theme = nullTheme.derive( 641 rule!Space( 642 (Space _) => rule!Frame( 643 backgroundColor = color("#123"), 644 ), 645 ), 646 ); 647 648 auto root = vspace(theme); 649 root.draw(); 650 651 assert(root.pickStyle.backgroundColor == Color.init); 652 653 } 654 655 /// Rules specify changes that are to be made to the node's style. 656 struct Rule { 657 658 alias StyleDelegate = Rule delegate(Node node) @safe; 659 alias loadTypeface = Style.loadTypeface; 660 661 public import fluid.theme; 662 663 /// Selector to filter items that should match this rule. 664 Selector selector; 665 666 /// Fields affected by this rule and their values. 667 StyleTemplate fields; 668 669 /// Callback for updating the style dynamically. May be null. 670 StyleDelegate styleDelegate; 671 672 /// Breadcrumbs, if any, assigned to nodes matching this rule. 673 Breadcrumbs breadcrumbs; 674 675 alias field(string name) = __traits(getMember, StyleTemplate, name); 676 alias FieldType(string name) = field!name.Type; 677 678 /// Returns true if the rule can be applied to the given node. 679 bool canApply(Node node) { 680 681 return selector.test(node); 682 683 } 684 685 /// Combine with another rule. Applies dynamic rules immediately. 686 bool apply(Node node, ref Rule rule) { 687 688 // Test against the selector 689 if (!canApply(node)) return false; 690 691 // Apply changes 692 fields.apply(rule.fields); 693 694 // Load breadcrumbs 695 rule.breadcrumbs ~= breadcrumbs; 696 697 // Check for delegates 698 if (styleDelegate) { 699 700 // Run and apply the delegate 701 styleDelegate(node).apply(node, rule); 702 703 } 704 705 return true; 706 707 } 708 709 /// Apply this rule on the given style. 710 /// Returns: True if applied, false if not. 711 bool apply(Node node, ref Style style) { 712 713 // Apply the rule 714 if (!applyStatic(node, style)) return false; 715 716 // Check for delegates 717 if (styleDelegate) { 718 719 // Run and apply the delegate 720 styleDelegate(node).apply(node, style); 721 722 } 723 724 return true; 725 726 } 727 728 /// Apply this rule on the given style. Ignores dynamic styles. 729 /// Returns: True if applied, false if not. 730 bool applyStatic(Node node, ref Style style) { 731 732 // Test against the selector 733 if (!canApply(node)) return false; 734 735 // Apply changes 736 fields.apply(style); 737 738 // Load breadcrumbs 739 style.breadcrumbs ~= breadcrumbs; 740 741 return true; 742 743 } 744 745 } 746 747 /// Branch out in a rule to apply styling based on a runtime condition. 748 WhenRule!predicate when(alias predicate, Args...)(Args args) { 749 750 return WhenRule!predicate(rule(args)); 751 752 } 753 754 struct WhenRule(alias dg) { 755 756 import std.functional; 757 758 /// Function to evaluate to test if the rule should be applied. 759 alias predicate = unaryFun!dg; 760 761 /// Rule to apply. 762 Rule rule; 763 764 /// Rule to apply when the predicate fails. 765 Rule alternativeRule; 766 767 /// Specify rule to apply in case the predicate fails. An `else` branch. 768 WhenRule otherwise(Args...)(Args args) { 769 770 // TODO else if? 771 772 auto result = this; 773 result.alternativeRule = .rule(args); 774 return result; 775 776 } 777 778 } 779 780 unittest { 781 782 import fluid.label; 783 784 auto myTheme = Theme( 785 rule!Label( 786 Rule.textColor = color!"100", 787 Rule.backgroundColor = color!"aaa", 788 789 when!"a.isEmpty"(Rule.textColor = color!"200"), 790 when!"a.text == `two`"(Rule.backgroundColor = color!"010") 791 .otherwise(Rule.backgroundColor = color!"020"), 792 ), 793 ); 794 795 auto io = new HeadlessBackend; 796 auto myLabel = label(myTheme, "one"); 797 798 myLabel.io = io; 799 myLabel.draw(); 800 801 assert(myLabel.pickStyle().textColor == color!"100"); 802 assert(myLabel.pickStyle().backgroundColor == color!"020"); 803 assert(myLabel.style.backgroundColor == color!"aaa"); 804 805 myLabel.text = ""; 806 807 assert(myLabel.pickStyle().textColor == color!"200"); 808 assert(myLabel.pickStyle().backgroundColor == color!"020"); 809 assert(myLabel.style.backgroundColor == color!"aaa"); 810 811 myLabel.text = "two"; 812 813 assert(myLabel.pickStyle().textColor == color!"100"); 814 assert(myLabel.pickStyle().backgroundColor == color!"010"); 815 assert(myLabel.style.backgroundColor == color!"aaa"); 816 817 } 818 819 /// Create a rule that affects the children of a node. To be placed inside a regular rule. 820 /// 821 /// A `children` rule creates a "breadcrumb" which is a tag applied to the node that tracks 822 /// all `children` rules affecting it, including all `children` rules it has spawned. Every node will 823 /// then activate corresponding rules 824 template children(T : Node = Node, tags...) { 825 826 ChildrenRule children(Ts...)(Ts fields) { 827 828 return ChildrenRule(rule!(T, tags)(fields)); 829 830 } 831 832 } 833 834 @("Basic children rules work") 835 unittest { 836 837 import fluid.space; 838 import fluid.frame; 839 import fluid.label; 840 import std.algorithm; 841 842 auto theme = nullTheme.derive( 843 844 // Labels are red by default 845 rule!Label( 846 textColor = color("#f00"), 847 ), 848 // Labels inside frames turn green 849 rule!Frame( 850 children!Label( 851 textColor = color("#0f0"), 852 ), 853 ), 854 855 ); 856 857 Label[2] greenLabels; 858 Label[2] redLabels; 859 860 auto root = vspace( 861 theme, 862 redLabels[0] = label("red"), 863 vframe( 864 greenLabels[0] = label("green"), 865 hspace( 866 greenLabels[1] = label("green"), 867 ), 868 ), 869 redLabels[1] = label("red"), 870 ); 871 872 root.draw(); 873 874 assert(redLabels[] .all!(a => a.pickStyle.textColor == color("#f00")), "All red labels are red"); 875 assert(greenLabels[].all!(a => a.pickStyle.textColor == color("#0f0")), "All green labels are green"); 876 877 } 878 879 @("Children rules can be nested") 880 unittest { 881 882 import fluid.space; 883 import fluid.frame; 884 import fluid.label; 885 import std.algorithm; 886 887 auto theme = nullTheme.derive( 888 889 // Labels are red by default 890 rule!Label( 891 textColor = color("#f00"), 892 ), 893 rule!Frame( 894 // Labels inside frames turn blue 895 children!Label( 896 textColor = color("#00f"), 897 ), 898 // But if nested further, they turn green 899 children!Frame( 900 textColor = color("#000"), 901 children!Label( 902 textColor = color("#0f0"), 903 ), 904 ), 905 ), 906 907 ); 908 909 Label[2] redLabels; 910 Label[3] blueLabels; 911 Label[4] greenLabels; 912 913 auto root = vspace( 914 theme, 915 redLabels[0] = label("Red"), 916 vframe( 917 blueLabels[0] = label("Blue"), 918 vframe( 919 greenLabels[0] = label("Green"), 920 vframe( 921 greenLabels[1] = label("Green"), 922 ), 923 ), 924 blueLabels[1] = label("Blue"), 925 vframe( 926 greenLabels[2] = label("Green"), 927 ) 928 ), 929 vspace( 930 vframe( 931 blueLabels[2] = label("Blue"), 932 vspace( 933 vframe( 934 greenLabels[3] = label("Green") 935 ), 936 ), 937 ), 938 redLabels[1] = label("Red"), 939 ), 940 ); 941 942 root.draw(); 943 944 assert(redLabels[] .all!(a => a.pickStyle.textColor == color("#f00")), "All red labels must be red"); 945 assert(blueLabels[] .all!(a => a.pickStyle.textColor == color("#00f")), "All blue labels must be blue"); 946 assert(greenLabels[].all!(a => a.pickStyle.textColor == color("#0f0")), "All green labels must be green"); 947 948 } 949 950 @("`children` rules work inside of `when`") 951 unittest { 952 953 import fluid.frame; 954 import fluid.label; 955 import fluid.button; 956 957 auto theme = nullTheme.derive( 958 rule!FrameButton( 959 children!Label( 960 textColor = color("#f00"), 961 ), 962 when!"a.isFocused"( 963 children!Label( 964 textColor = color("#0f0"), 965 ), 966 ), 967 ), 968 ); 969 970 FrameButton first, second; 971 Label firstLabel, secondLabel; 972 973 auto root = vframe( 974 theme, 975 first = vframeButton( 976 firstLabel = label("Hello"), 977 delegate { } 978 ), 979 second = vframeButton( 980 secondLabel = label("Hello"), 981 delegate { } 982 ), 983 ); 984 985 root.draw(); 986 987 assert(firstLabel.pickStyle.textColor == color("#f00")); 988 assert(secondLabel.pickStyle.textColor == color("#f00")); 989 990 first.focus(); 991 root.draw(); 992 993 assert(firstLabel.pickStyle.textColor == color("#0f0")); 994 assert(secondLabel.pickStyle.textColor == color("#f00")); 995 996 second.focus(); 997 root.draw(); 998 999 assert(firstLabel.pickStyle.textColor == color("#f00")); 1000 assert(secondLabel.pickStyle.textColor == color("#0f0")); 1001 1002 } 1003 1004 @("`children` rules work inside of delegates") 1005 unittest { 1006 1007 // Note: This is impractical; in reality this will allocate memory excessively. 1008 // This could be avoided by allocating all breadcrumbs on a stack. 1009 import fluid.frame; 1010 import fluid.label; 1011 import fluid.button; 1012 1013 class ColorFrame : Frame { 1014 1015 Color color; 1016 1017 this(Color color, Node[] nodes...) { 1018 this.color = color; 1019 super(nodes); 1020 } 1021 1022 } 1023 1024 auto theme = nullTheme.derive( 1025 rule!Label( 1026 textColor = color("#000"), 1027 ), 1028 rule!ColorFrame( 1029 (ColorFrame a) => rule( 1030 children!Label( 1031 textColor = a.color, 1032 ) 1033 ) 1034 ), 1035 ); 1036 1037 ColorFrame frame; 1038 Label target; 1039 Label sample; 1040 1041 auto root = vframe( 1042 theme, 1043 frame = new ColorFrame( 1044 color("#00f"), 1045 target = label("Colorful label"), 1046 ), 1047 sample = label("Never affected"), 1048 ); 1049 1050 root.draw(); 1051 1052 assert(target.pickStyle.textColor == color("#00f")); 1053 assert(sample.pickStyle.textColor == color("#000")); 1054 1055 frame.color = color("#0f0"), 1056 root.draw(); 1057 1058 assert(target.pickStyle.textColor == color("#0f0")); 1059 assert(sample.pickStyle.textColor == color("#000")); 1060 1061 } 1062 1063 @("Children rules can contain `when` clauses and delegates") 1064 unittest { 1065 1066 import fluid.frame; 1067 import fluid.space; 1068 import fluid.button; 1069 1070 // Focused button turns red, or green if inside of a frame 1071 auto theme = nullTheme.derive( 1072 rule!Frame( 1073 children!Button( 1074 when!"a.isFocused"( 1075 textColor = color("#0f0"), 1076 ), 1077 (Node b) => rule( 1078 backgroundColor = color("#123"), 1079 ), 1080 ), 1081 ), 1082 rule!Button( 1083 textColor = color("#000"), 1084 backgroundColor = color("#000"), 1085 when!"a.isFocused"( 1086 textColor = color("#f00"), 1087 ), 1088 ), 1089 ); 1090 1091 Button greenButton; 1092 Button redButton; 1093 1094 auto root = vspace( 1095 theme, 1096 vframe( 1097 greenButton = button("Green", delegate { }), 1098 ), 1099 redButton = button("Red", delegate { }), 1100 ); 1101 1102 root.draw(); 1103 1104 assert(greenButton.pickStyle.textColor == color("#000")); 1105 assert(greenButton.pickStyle.backgroundColor == color("#123")); 1106 assert(redButton.pickStyle.textColor == color("#000")); 1107 assert(redButton.pickStyle.backgroundColor == color("#000")); 1108 1109 greenButton.focus(); 1110 root.draw(); 1111 1112 assert(greenButton.isFocused); 1113 assert(greenButton.pickStyle.textColor == color("#0f0")); 1114 assert(greenButton.pickStyle.backgroundColor == color("#123")); 1115 assert(redButton.pickStyle.textColor == color("#000")); 1116 assert(redButton.pickStyle.backgroundColor == color("#000")); 1117 1118 redButton.focus(); 1119 root.draw(); 1120 1121 assert(greenButton.pickStyle.textColor == color("#000")); 1122 assert(greenButton.pickStyle.backgroundColor == color("#123")); 1123 assert(redButton.pickStyle.textColor == color("#f00")); 1124 assert(redButton.pickStyle.backgroundColor == color("#000")); 1125 1126 } 1127 1128 /// A version of `Rule` that affects children. 1129 struct ChildrenRule { 1130 1131 Rule rule; 1132 1133 } 1134 1135 struct Breadcrumbs { 1136 1137 alias Key = size_t; 1138 1139 /// All rules activated by this instance. 1140 Rule[][] crumbs; 1141 1142 /// Cached children instances. 1143 Breadcrumbs[Key] children; 1144 1145 bool opCast(T : bool)() const { 1146 1147 return this !is this.init; 1148 1149 } 1150 1151 /// Get an key for the given ruleset. 1152 static Key key(Rule[] rules) { 1153 1154 return cast(Key) rules.ptr; 1155 1156 } 1157 1158 /// Apply the breadcrumbs on the given node. Runs static rules only. 1159 void applyStatic(Node node, ref Style style) { 1160 1161 foreach (rules; crumbs) { 1162 1163 foreach (rule; rules) { 1164 1165 // Apply the styles 1166 // applyStatic tests compatibility automatically 1167 rule.applyStatic(node, style); 1168 1169 } 1170 1171 } 1172 1173 } 1174 1175 /// Apply the breadcrumbs on the given node. Runs dynamic rules only. 1176 void applyDynamic(Node node, ref Style style) { 1177 1178 foreach (rules; crumbs) { 1179 1180 foreach (rule; rules) { 1181 1182 if (rule.styleDelegate && rule.canApply(node)) { 1183 1184 rule.styleDelegate(node).apply(node, style); 1185 1186 } 1187 1188 } 1189 1190 } 1191 1192 } 1193 1194 /// Combine with another breadcrumbs instance. 1195 /// 1196 /// This breadcrumb will now point to the same breadcrumb as the one given, but the chain will be combined to 1197 /// include both of them. 1198 ref Breadcrumbs opOpAssign(string op : "~")(Breadcrumbs other) return { 1199 1200 // Stop if there's nothing to do 1201 if (!other) return this; 1202 1203 foreach (rules; other.crumbs) { 1204 1205 // Skip empty crumbs 1206 if (rules.length == 0) continue; 1207 1208 // Loop up the entry in the cache 1209 // If one isn't present, create a new one with the ruleset appended 1210 this = children.require(key(rules), Breadcrumbs(crumbs ~ rules)); 1211 1212 } 1213 1214 return this; 1215 1216 } 1217 1218 } 1219 1220 struct StyleTemplate { 1221 1222 alias fields = NoDuplicates!(getSymbolsByUDA!(Style, Style.Themable)); 1223 1224 // Create fields for every themable item 1225 static foreach (field; fields) { 1226 1227 static if (!isFunction!(typeof(field))) 1228 mixin(q{ FieldValue!(typeof(field)) }, __traits(identifier, field), ";"); 1229 1230 } 1231 1232 /// Update the given style using this template. 1233 void apply(ref Style style) { 1234 1235 // TODO only iterate on fields that have changed 1236 static foreach (field; fields) {{ 1237 1238 auto newValue = mixin("this.", __traits(identifier, field)); 1239 1240 newValue.apply(__traits(child, style, field)); 1241 1242 }} 1243 1244 } 1245 1246 /// Combine with another style template, applying all local rules on the other template. 1247 void apply(ref StyleTemplate style) { 1248 1249 static foreach (i, field; fields) { 1250 1251 this.tupleof[i].apply(style.tupleof[i]); 1252 1253 } 1254 1255 } 1256 1257 string toString()() const @trusted { 1258 1259 string[] items; 1260 1261 static foreach (field; fields) {{ 1262 1263 enum name = __traits(identifier, field); 1264 auto value = mixin("this.", name); 1265 1266 if (value.isSet) 1267 items ~= format("%s: %s", name, value); 1268 1269 }} 1270 1271 return format("StyleTemplate(%-(%s, %))", items); 1272 1273 } 1274 1275 } 1276 1277 /// `Field` allows defining and performing partial changes to members of Style. 1278 struct Field(string fieldName, T) { 1279 1280 enum name = fieldName; 1281 alias Type = T; 1282 1283 FieldValue!T value; 1284 1285 static Field make(Item, size_t n)(Item[n] value) { 1286 1287 Field field; 1288 field.value = value; 1289 return field; 1290 1291 } 1292 1293 static Field make(T)(T value) 1294 if (!isStaticArray!T) 1295 do { 1296 1297 Field field; 1298 field.value = value; 1299 return field; 1300 1301 } 1302 1303 static Field make() { 1304 1305 return Field(); 1306 1307 } 1308 1309 /// Apply on a style template. 1310 void apply(ref StyleTemplate style) { 1311 1312 value.apply(__traits(child, style, Rule.field!fieldName)); 1313 1314 } 1315 1316 // Operators for structs 1317 static if (is(T == struct)) { 1318 1319 template opDispatch(string field) 1320 if (__traits(hasMember, T, field)) 1321 { 1322 1323 Field opDispatch(Input)(Input input) return { 1324 1325 __traits(getMember, value, field) = input; 1326 return this; 1327 1328 } 1329 1330 } 1331 1332 } 1333 1334 // Operators for arrays 1335 static if (isStaticArray!T) { 1336 1337 private size_t[2] slice; 1338 1339 Field opAssign(Input, size_t n)(Input[n] input) return { 1340 1341 value[slice] = input; 1342 return this; 1343 1344 } 1345 1346 Field opAssign(Input)(Input input) return 1347 if (!isStaticArray!Input) 1348 do { 1349 1350 value[slice] = input; 1351 return this; 1352 1353 } 1354 1355 inout(Field) opIndex(size_t i) return inout { 1356 1357 return inout Field(value, [i, i+1]); 1358 1359 } 1360 1361 inout(Field) opIndex(return inout Field slice) const { 1362 1363 return slice; 1364 1365 } 1366 1367 inout(Field) opSlice(size_t dimension : 0)(size_t i, size_t j) return inout { 1368 1369 return Field(value, [i, j]); 1370 1371 } 1372 1373 } 1374 1375 } 1376 1377 unittest { 1378 1379 auto typeface = Style.loadTypeface(4); 1380 auto sample = Rule.typeface = typeface; 1381 1382 const originalTypeface = typeface; 1383 1384 assert(sample.name == "typeface"); 1385 1386 auto target = Style.loadTypeface(3); 1387 sample.value.apply(target); 1388 1389 assert(target is typeface); 1390 assert(typeface is originalTypeface); 1391 1392 } 1393 1394 unittest { 1395 1396 auto sampleField = Rule.margin = 4; 1397 1398 assert(sampleField.name == "margin"); 1399 assert(sampleField.slice == [0, 0]); 1400 1401 float[4] field = [1, 1, 1, 1]; 1402 sampleField.value.apply(field); 1403 1404 assert(field == [4, 4, 4, 4]); 1405 1406 } 1407 1408 unittest { 1409 1410 auto sampleField = Rule.margin.sideX = 8; 1411 1412 assert(sampleField.name == "margin"); 1413 assert(sampleField.slice == [0, 2]); 1414 1415 float[4] field = [1, 1, 1, 1]; 1416 sampleField.value.apply(field); 1417 1418 assert(field == [8, 8, 1, 1]); 1419 1420 } 1421 1422 template FieldValue(T) { 1423 1424 // Struct 1425 static if (is(T == struct)) 1426 alias FieldValue = FieldValueStruct!T; 1427 1428 // Static array 1429 else static if (is(T : E[n], E, size_t n)) 1430 alias FieldValue = FieldValueStaticArray!(E, n); 1431 1432 // Others 1433 else alias FieldValue = FieldValueOther!T; 1434 1435 } 1436 1437 private struct FieldValueStruct(T) { 1438 1439 alias Type = T; 1440 alias ExpandedType = staticMap!(FieldValue, typeof(Type.tupleof)); 1441 1442 ExpandedType value; 1443 bool isSet; 1444 1445 FieldValueStruct opAssign(FieldValueStruct value) { 1446 1447 this.value = value.value; 1448 this.isSet = value.isSet; 1449 1450 return this; 1451 1452 } 1453 1454 Type opAssign(Type value) { 1455 1456 // Mark as set 1457 isSet = true; 1458 1459 // Assign each field 1460 foreach (i, ref field; this.value) { 1461 1462 field = value.tupleof[i]; 1463 1464 } 1465 1466 return value; 1467 1468 } 1469 1470 template opDispatch(string name) 1471 if (__traits(hasMember, Type, name)) 1472 { 1473 1474 T opDispatch(T)(T value) { 1475 1476 enum i = staticIndexOf!(name, FieldNameTuple!Type); 1477 1478 // Mark as set 1479 isSet = true; 1480 1481 // Assign the field 1482 this.value[i] = value; 1483 1484 return value; 1485 1486 } 1487 1488 } 1489 1490 /// Change the given value to match the requested change. 1491 void apply(ref Type value) { 1492 1493 if (!isSet) return; 1494 1495 foreach (i, field; this.value) { 1496 field.apply(value.tupleof[i]); 1497 } 1498 1499 } 1500 1501 /// Change the given value to match the requested change. 1502 void apply(ref FieldValueStruct value) { 1503 1504 if (!isSet) return; 1505 1506 value.isSet = true; 1507 1508 foreach (i, field; this.value) { 1509 field.apply(value.value[i]); 1510 } 1511 1512 } 1513 1514 } 1515 1516 private struct FieldValueStaticArray(E, size_t n) { 1517 1518 alias Type = E[n]; 1519 alias ExpandedType = FieldValue!E[n]; 1520 1521 ExpandedType value; 1522 bool isSet; 1523 1524 FieldValueStaticArray opAssign(FieldValueStaticArray value) { 1525 1526 this.value = value.value; 1527 this.isSet = value.isSet; 1528 return this; 1529 1530 } 1531 1532 Item[n] opAssign(Item, size_t n)(Item[n] value) { 1533 1534 // Mark as changed 1535 isSet = true; 1536 1537 // Assign each field 1538 foreach (i, ref field; this.value.tupleof) { 1539 1540 field = value.tupleof[i]; 1541 1542 } 1543 1544 return value; 1545 1546 } 1547 1548 Input opAssign(Input)(Input value) 1549 if (!isStaticArray!Input) 1550 do { 1551 1552 // Implicit cast 1553 Type newValue = value; 1554 1555 opAssign(newValue); 1556 1557 return value; 1558 1559 } 1560 1561 Input opIndexAssign(Input)(Input input, size_t index) { 1562 1563 isSet = true; 1564 value[index] = input; 1565 return input; 1566 1567 } 1568 1569 Input[n] opIndexAssign(Input, size_t n)(Input[n] input, size_t[2] indices) { 1570 1571 assert(indices[1] - indices[0] == n, "Invalid slice"); 1572 1573 isSet = true; 1574 foreach (i, ref field; value[indices[0] .. indices[1]]) { 1575 field = input[i]; 1576 } 1577 1578 return input; 1579 1580 } 1581 1582 auto opIndexAssign(Input)(Input input, size_t[2] indices) 1583 if (!isStaticArray!Input) 1584 do { 1585 1586 isSet = true; 1587 return value[indices[0] .. indices[1]] = FieldValue!E(input, true); 1588 1589 } 1590 1591 size_t[2] opSlice(size_t i, size_t j) const { 1592 1593 return [i, j]; 1594 1595 } 1596 1597 /// Change the given value to match the requested change. 1598 void apply(ref Type value) { 1599 1600 if (!isSet) return; 1601 1602 foreach (i, field; this.value.tupleof) { 1603 field.apply(value.tupleof[i]); 1604 } 1605 1606 } 1607 1608 /// Change the given value to match the requested change. 1609 void apply(ref FieldValueStaticArray value) { 1610 1611 if (!isSet) return; 1612 1613 value.isSet = true; 1614 1615 foreach (i, field; this.value.tupleof) { 1616 field.apply(value.value[i]); 1617 } 1618 1619 } 1620 1621 string toString() { 1622 1623 Type output; 1624 apply(output); 1625 return format!"%s"(output); 1626 1627 } 1628 1629 } 1630 1631 private struct FieldValueOther(T) { 1632 1633 alias Type = T; 1634 1635 Type value; 1636 bool isSet; 1637 1638 FieldValueOther opAssign(FieldValueOther value) { 1639 1640 this.value = value.value; 1641 this.isSet = value.isSet; 1642 1643 return this; 1644 1645 } 1646 1647 Type opAssign(Input)(Input value) { 1648 1649 // Mark as set 1650 isSet = true; 1651 1652 return this.value = value; 1653 1654 } 1655 1656 /// Change the given value to match the requested change. 1657 void apply(ref Type value) { 1658 1659 if (!isSet) return; 1660 1661 value = this.value; 1662 1663 } 1664 1665 /// Apply another modification to a field. 1666 void apply(ref FieldValueOther value) { 1667 1668 if (!isSet) return; 1669 1670 value.value = this.value; 1671 value.isSet = true; 1672 1673 } 1674 1675 string toString() const @trusted { 1676 1677 return format!"%s"(value); 1678 1679 } 1680 1681 } 1682 1683 @("Rules using tags from different enums do not collide") 1684 unittest { 1685 1686 import fluid.label; 1687 import fluid.space; 1688 1689 @NodeTag enum Foo { tag } 1690 @NodeTag enum Bar { tag } 1691 1692 auto theme = nullTheme.derive( 1693 rule!Label( 1694 textColor = color("#f00"), 1695 ), 1696 rule!(Label, Foo.tag)( 1697 textColor = color("#0f0"), 1698 ), 1699 ); 1700 1701 Label fooLabel, barLabel; 1702 1703 auto root = vspace( 1704 theme, 1705 fooLabel = label(.tags!(Foo.tag), "foo"), 1706 barLabel = label(.tags!(Bar.tag), "bar"), 1707 ); 1708 1709 root.draw(); 1710 1711 assert(fooLabel.pickStyle().textColor == color("#0f0")); 1712 assert(barLabel.pickStyle().textColor == color("#f00")); 1713 1714 }