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