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 }