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 }