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 
11 import fluid.node;
12 import fluid.style;
13 import fluid.backend;
14 import fluid.structs;
15 
16 
17 @safe:
18 
19 
20 deprecated("Styles have been reworked and defineStyles is now a no-op. To be removed in 0.8.0.") {
21     mixin template defineStyles(args...) { }
22     mixin template DefineStyles(args...) { }
23 }
24 
25 deprecated("makeTheme is now a no-op. Use `Theme()` and refer to the changelog for updates. To be removed in 0.8.0.")
26 Theme makeTheme(string s, Ts...)(Ts) {
27 
28     return Theme.init;
29 
30 }
31 
32 /// Node theme.
33 struct Theme {
34 
35     Rule[][TypeInfo_Class] rules;
36 
37     /// Create a new theme using the given rules.
38     this(Rule[] rules...) {
39 
40         // Inherit from default theme
41         this(fluidDefaultTheme.rules.dup);
42         add(rules);
43 
44     }
45 
46     /// Create a theme using the given set of rules.
47     this(Rule[][TypeInfo_Class] rules) {
48 
49         this.rules = rules;
50 
51     }
52 
53     /// Check if the theme was initialized.
54     bool opCast(T : bool)() const {
55 
56         return rules !is null;
57 
58     }
59 
60     /// Create a new theme that derives from another.
61     ///
62     /// Note: This doesn't duplicate rules. If rules are changed or reassigned, they will may the parent theme. In a
63     /// typical scenario you only add new rules.
64     Theme derive(Rule[] rules...) {
65 
66         auto newTheme = this.dup;
67         newTheme.add(rules);
68         return newTheme;
69 
70     }
71 
72     /// Add rules to the theme.
73     void add(Rule[] rules...) {
74 
75         foreach (rule; rules) {
76 
77             this.rules[rule.selector.type] ~= rule;
78 
79         }
80 
81     }
82 
83     /// Make the node use this theme.
84     void apply(Node node) {
85 
86         node.theme = this;
87 
88     }
89 
90     /// Apply this theme on the given style.
91     /// Returns: An array of delegates used to update the style at runtime.
92     Rule.StyleDelegate[] apply(Node node, ref Style style) {
93 
94         Rule.StyleDelegate[] dgs;
95 
96         void applyFor(TypeInfo_Class ti) {
97 
98             // Inherit from parents
99             if (ti.base) applyFor(ti.base);
100 
101             // Find matching rules
102             if (auto rules = ti in rules) {
103 
104                 // Test against every rule
105                 foreach (rule; *rules) {
106 
107                     // Run the rule, and add the callback if any
108                     if (rule.applyStatic(node, style) && rule.styleDelegate) {
109 
110                         dgs ~= rule.styleDelegate;
111 
112                     }
113 
114                 }
115 
116             }
117 
118         }
119 
120         applyFor(typeid(node));
121 
122         return dgs;
123 
124     }
125 
126     /// Duplicate the theme. This is not recursive; rules are not copied.
127     Theme dup() {
128 
129         return Theme(rules.dup);
130 
131     }
132 
133 }
134 
135 unittest {
136 
137     import fluid.label;
138 
139     Theme theme;
140 
141     with (Rule)
142     theme.add(
143         rule!Label(
144             textColor = color!"#abc",
145         ),
146     );
147 
148     auto io = new HeadlessBackend;
149     auto root = label(theme, "placeholder");
150     root.io = io;
151 
152     root.draw();
153     io.assertTexture(root.text.texture.chunks[0], Vector2(0, 0), color!"#fff");
154     assert(root.text.texture.chunks[0].palette[0] == color("#abc"));
155 
156 }
157 
158 unittest {
159 
160     import fluid.frame;
161 
162     auto frameRule = rule!Frame(
163         Rule.margin.sideX = 8,
164         Rule.margin.sideY = 4,
165     );
166     auto theme = nullTheme.derive(frameRule);
167 
168     // Test selector
169     assert(frameRule.selector.type == typeid(Frame));
170     assert(frameRule.selector.tags.empty);
171 
172     // Test fields
173     auto style = Style.init;
174     frameRule.apply(vframe(), style);
175     assert(style.margin == [8, 8, 4, 4]);
176     assert(style.padding == style.init.padding);
177     assert(style.textColor == style.textColor.init);
178 
179     // No dynamic rules
180     assert(rule.styleDelegate is null);
181 
182     auto io = new HeadlessBackend;
183     auto root = vframe(theme);
184     root.io = io;
185 
186     root.draw();
187 
188     assert(root.style == style);
189 
190 }
191 
192 unittest {
193 
194     // Inheritance
195 
196     import fluid.label;
197     import fluid.button;
198 
199     auto myTheme = nullTheme.derive(
200         rule!Node(
201             Rule.margin.sideX = 8,
202         ),
203         rule!Label(
204             Rule.margin.sideTop = 6,
205         ),
206         rule!Button(
207             Rule.margin.sideBottom = 4,
208         ),
209     );
210 
211     auto style = Style.init;
212     myTheme.apply(button("", delegate { }), style);
213 
214     assert(style.margin == [8, 8, 6, 4]);
215 
216 }
217 
218 unittest {
219 
220     // Copying rules & dynamic rules
221 
222     import fluid.label;
223     import fluid.button;
224 
225     auto myRule = rule!Label(
226         Rule.textColor = color!"011",
227         Rule.backgroundColor = color!"faf",
228         (Label node) => node.isDisabled
229             ? rule(Rule.tint = color!"000a")
230             : rule()
231     );
232 
233     auto myTheme = Theme(
234         rule!Label(
235             myRule,
236         ),
237         rule!Button(
238             myRule,
239             Rule.textColor = color!"012",
240         ),
241     );
242 
243     auto style = Style.init;
244     auto myLabel = label("");
245 
246     // Apply the style, including dynamic rules
247     auto cbs = myTheme.apply(myLabel, style);
248     assert(cbs.length == 1);
249     cbs[0](myLabel).apply(myLabel, style);
250 
251     assert(style.textColor == color!"011");
252     assert(style.backgroundColor == color!"faf");
253     assert(style.tint == Style.init.tint);
254 
255     // Disable the node and apply again, it should change nothing
256     myLabel.disable();
257     myTheme.apply(myLabel, style);
258     assert(style.tint == Style.init.tint);
259 
260     // Apply the callback, tint should change
261     cbs[0](myLabel).apply(myLabel, style);
262     assert(style.tint == color!"000a");
263 
264 }
265 
266 unittest {
267 
268     import fluid.label;
269 
270     auto myLabel = label("");
271 
272     void testMargin(Rule rule, float[4] margin) {
273 
274         auto style = Style.init;
275 
276         rule.apply(myLabel, style);
277 
278         assert(style.margin == margin);
279 
280     }
281 
282     with (Rule) {
283 
284         testMargin(rule(margin = 2), [2, 2, 2, 2]);
285         testMargin(rule(margin.sideX = 2), [2, 2, 0, 0]);
286         testMargin(rule(margin.sideY = 2), [0, 0, 2, 2]);
287         testMargin(rule(margin.sideTop = 2), [0, 0, 2, 0]);
288         testMargin(rule(margin.sideBottom = 2), [0, 0, 0, 2]);
289         testMargin(rule(margin.sideX = 2, margin.sideY = 4), [2, 2, 4, 4]);
290         testMargin(rule(margin = [1, 2, 3, 4]), [1, 2, 3, 4]);
291         testMargin(rule(margin.sideX = [1, 2]), [1, 2, 0, 0]);
292 
293     }
294 
295 }
296 
297 unittest {
298 
299     import std.math;
300     import fluid.label;
301 
302     auto myRule = rule(
303         Rule.opacity = 0.5,
304     );
305     auto style = Style.init;
306 
307     myRule.apply(label(""), style);
308 
309     assert(style.opacity.isClose(127/255f));
310     assert(style.tint == color!"ffffff7f");
311 
312     auto secondRule = rule(
313         Rule.tint = color!"abc",
314         Rule.opacity = 0.6,
315     );
316 
317     style = Style.init;
318     secondRule.apply(label(""), style);
319 
320     assert(style.opacity.isClose(153/255f));
321     assert(style.tint == color!"abc9");
322 
323 }
324 
325 // Define field setters for each field, to use by importing or through `Rule.property = value`
326 static foreach (field; StyleTemplate.fields) {
327 
328     mixin(format!q{
329         alias %1$s = Field!("%1$s", typeof(field)).make;
330     }(__traits(identifier, field)));
331 
332 }
333 
334 // Separately define opacity for convenience
335 auto opacity(float value) {
336 
337     import std.algorithm : clamp;
338 
339     return tint.a = cast(ubyte) clamp(value * ubyte.max, ubyte.min, ubyte.max);
340 
341 }
342 
343 /// Selector is used to pick a node based on its type and specified tags.
344 struct Selector {
345 
346     /// Type of the node to match.
347     TypeInfo_Class type;
348 
349     /// Tags needed by the selector.
350     TagList tags;
351 
352     /// If true, this selector will reject any match.
353     bool rejectAll;
354 
355     /// Returns a selector that doesn't match anything
356     static Selector none() {
357 
358         Selector result;
359         result.rejectAll = true;
360         return result;
361 
362     }
363 
364     /// Test if the selector matches given node.
365     bool test(Node node) {
366 
367         return !rejectAll
368             && testType(typeid(node))
369             && testTags(node.tags);
370 
371     }
372 
373     /// Test if the given type matches the selector.
374     bool testType(TypeInfo_Class type) {
375 
376         return !this.type || this.type.isBaseOf(type);
377 
378     }
379 
380     unittest {
381 
382         import fluid.input;
383         import fluid.label;
384         import fluid.button;
385 
386         auto myLabel = label("my label");
387         auto myButton = button("my button", delegate { });
388 
389         auto selector = Selector(typeid(Node));
390 
391         assert(selector.test(myLabel));
392         assert(selector.test(myButton));
393 
394         auto anotherSelector = Selector(typeid(Button));
395 
396         assert(!anotherSelector.test(myLabel));
397         assert(anotherSelector.test(myButton));
398 
399         auto noSelector = Selector();
400 
401         assert(noSelector.test(myLabel));
402         assert(noSelector.test(myButton));
403 
404     }
405 
406     /// True if all tags in this selector are present on the given node.
407     bool testTags(TagList tags) {
408 
409         // TODO linear search if there's only one tag
410         return this.tags.intersect(tags).walkLength == this.tags.length;
411 
412     }
413 
414     unittest {
415 
416         import fluid.label;
417 
418         @NodeTag enum good;
419         @NodeTag enum bad;
420 
421         auto selector = Selector(typeid(Label)).addTags!good;
422 
423         assert(selector.test(label(.tags!good, "")));
424         assert(!selector.test(label(.tags!bad, "")));
425         assert(!selector.test(label("")));
426 
427     }
428 
429     /// Create a new selector requiring the given set of tags.
430     Selector addTags(tags...)() {
431 
432         return Selector(type, this.tags.add!tags);
433 
434     }
435 
436 }
437 
438 /// Create a style rule for the given node.
439 ///
440 /// Template parameters are used to select the node the rule applies to, based on its type and the tags it has. Regular
441 /// parameters define the changes made by the rule. These are created by using automatically defined members of `Rule`,
442 /// which match names of `Style` fields. For example, to change the property `Style.textColor`, one would assign
443 /// `Rule.textColor` inside this parameter list.
444 ///
445 /// ---
446 /// rule!Label(
447 ///     Rule.textColor = color("#fff"),
448 ///     Rule.backgroundColor = color("#000"),
449 /// )
450 /// ---
451 ///
452 /// It is also possible to pass a `when` subrule to apply changes based on runtime conditions:
453 ///
454 /// ---
455 /// rule!Button(
456 ///     Rule.backgroundColor = color("#fff"),
457 ///     when!"a.isHovered"(
458 ///         Rule.backgroundColor = color("#ccc"),
459 ///     )
460 /// )
461 /// ---
462 ///
463 /// If some directives are repeated across different rules, they can be reused:
464 ///
465 /// ---
466 /// myRule = rule(
467 ///     Rule.textColor = color("#000"),
468 /// ),
469 /// rule!Button(
470 ///     myRule,
471 ///     Rule.backgroundColor = color("#fff"),
472 /// )
473 /// ---
474 ///
475 /// Moreover, rules respect inheritance. Since `Button` derives from `Label` and `Node`, `rule!Label` will also apply
476 /// to buttons, and so will `rule!Node`.
477 ///
478 /// For more advanced use-cases, it is possible to directly pass a delegate that accepts a node and returns a
479 /// subrule.
480 ///
481 /// ---
482 /// rule!Button(
483 ///     a => rule(
484 ///         Rule.backgroundColor = pickColor(),
485 ///     )
486 /// )
487 /// ---
488 ///
489 /// It is recommended to use the `with (Rule)` statement to make rule definitions clearer.
490 template rule(T : Node = Node, tags...) {
491 
492     Rule rule(Ts...)(Ts fields) {
493 
494         enum isWhenRule(alias field) = is(typeof(field) : WhenRule!dg, alias dg);
495         enum isDynamicRule(alias field) = isCallable!field || isWhenRule!field || is(typeof(field) : Rule);
496 
497         Rule result;
498 
499         // Create the selector
500         result.selector = Selector(typeid(T)).addTags!tags;
501 
502         // Load fields
503         static foreach (i, field; fields) {{
504 
505             // Directly assigned field
506             static if (is(typeof(field) : Field!(fieldName, T), string fieldName, T)) {
507 
508                 // Add to the result
509                 field.apply(result.fields);
510 
511             }
512 
513             // Copy from another rule
514             else static if (is(typeof(field) : Rule)) {
515 
516                 assert(field.selector.testType(typeid(T)),
517                     format!"Cannot paste rule for %s into a rule for %s"(field.selector.type, typeid(T)));
518 
519                 // Copy fields
520                 field.fields.apply(result.fields);
521                 // Also add delegates below...
522 
523 
524             }
525 
526             // Dynamic rule
527             else static if (isDynamicRule!field) { }
528 
529             else static assert(false, format!"Unrecognized type %s (argument index %s)"(typeof(field).stringof, i));
530 
531         }}
532 
533         // Load delegates
534         alias delegates = Filter!(isDynamicRule, fields);
535 
536         // Build the dynamic rule delegate
537         static if (delegates.length)
538         result.styleDelegate = (Node node) {
539 
540             Rule dynamicResult;
541 
542             // Cast the node into proper type
543             auto castNode = cast(T) node;
544             assert(castNode, "styleDelegate was passed an invalid node");
545 
546             static foreach (dg; delegates) {
547 
548                 // A "when" rule
549                 static if (isWhenRule!dg) {
550 
551                     // Test the predicate before applying the result
552                     if (dg.predicate(castNode)) {
553 
554                         dg.rule.apply(node, dynamicResult);
555 
556                     }
557 
558                     // Apply the alternative if predicate fails
559                     else dg.alternativeRule.apply(node, dynamicResult);
560 
561                 }
562 
563                 // Regular rule, forward to its delegate
564                 else static if (is(typeof(dg) : Rule)) {
565 
566                     if (dg.styleDelegate)
567                     dynamicResult = dg.styleDelegate(node);
568 
569                 }
570 
571                 // Use the delegate and apply the result on the template of choice
572                 else dg(castNode).apply(node, dynamicResult);
573 
574             }
575 
576             return dynamicResult;
577 
578         };
579 
580         return result;
581 
582     }
583 
584 }
585 
586 @trusted
587 unittest {
588 
589     // Rule copying semantics
590 
591     import fluid.label;
592     import fluid.button;
593     import std.exception;
594     import core.exception : AssertError;
595 
596     auto generalRule = rule(
597         Rule.textColor = color!"#001",
598     );
599     auto buttonRule = rule!Button(
600         Rule.backgroundColor = color!"#002",
601         generalRule,
602     );
603     assertThrown!AssertError(
604         rule!Label(buttonRule),
605         "Label rule cannot inherit from a Button rule."
606     );
607 
608     assertNotThrown(rule!Button(buttonRule));
609     assertNotThrown(rule!Button(rule!Label()));
610 
611 }
612 
613 /// Rules specify changes that are to be made to the node's style.
614 struct Rule {
615 
616     alias StyleDelegate = Rule delegate(Node node) @safe;
617     alias loadTypeface = Style.loadTypeface;
618 
619     public import fluid.theme;
620 
621     /// Selector to filter items that should match this rule.
622     Selector selector;
623 
624     /// Fields affected by this rule and their values.
625     StyleTemplate fields;
626 
627     /// Callback for updating the style dynamically. May be null.
628     StyleDelegate styleDelegate;
629 
630     alias field(string name) = __traits(getMember, StyleTemplate, name);
631     alias FieldType(string name) = field!name.Type;
632 
633     /// Combine with another rule. Applies dynamic rules immediately.
634     bool apply(Node node, ref Rule rule) {
635 
636         // Test against the selector
637         if (!selector.test(node)) return false;
638 
639         // Apply changes
640         fields.apply(rule.fields);
641 
642         // Check for delegates
643         if (styleDelegate) {
644 
645             // Run and apply the delegate
646             styleDelegate(node).apply(node, rule);
647 
648         }
649 
650         return true;
651 
652     }
653 
654     /// Apply this rule on the given style.
655     /// Returns: True if applied, false if not.
656     bool apply(Node node, ref Style style) {
657 
658         // Apply the rule
659         if (!applyStatic(node, style)) return false;
660 
661         // Check for delegates
662         if (styleDelegate) {
663 
664             // Run and apply the delegate
665             styleDelegate(node).apply(node, style);
666 
667         }
668 
669         return true;
670 
671     }
672 
673     /// Apply this rule on the given style. Ignores dynamic styles.
674     /// Returns: True if applied, false if not.
675     bool applyStatic(Node node, ref Style style) {
676 
677         // Test against the selector
678         if (!selector.test(node)) return false;
679 
680         // Apply changes
681         fields.apply(style);
682 
683         // TODO dynamic rules
684 
685         return true;
686 
687     }
688 
689 }
690 
691 /// Branch out in a rule to apply styling based on a runtime condition.
692 WhenRule!predicate when(alias predicate, Args...)(Args args) {
693 
694     return WhenRule!predicate(rule(args));
695 
696 }
697 
698 struct WhenRule(alias dg) {
699 
700     import std.functional;
701 
702     /// Function to evaluate to test if the rule should be applied.
703     alias predicate = unaryFun!dg;
704 
705     /// Rule to apply.
706     Rule rule;
707 
708     /// Rule to apply when the predicate fails.
709     Rule alternativeRule;
710 
711     /// Specify rule to apply in case the predicate fails. An `else` branch.
712     WhenRule otherwise(Args...)(Args args) {
713 
714         // TODO else if?
715 
716         auto result = this;
717         result.alternativeRule = .rule(args);
718         return result;
719 
720     }
721 
722 
723 }
724 
725 unittest {
726 
727     import fluid.label;
728 
729     auto myTheme = Theme(
730         rule!Label(
731             Rule.textColor = color!"100",
732             Rule.backgroundColor = color!"aaa",
733 
734             when!"a.isEmpty"(Rule.textColor = color!"200"),
735             when!"a.text == `two`"(Rule.backgroundColor = color!"010")
736                 .otherwise(Rule.backgroundColor = color!"020"),
737         ),
738     );
739 
740     auto io = new HeadlessBackend;
741     auto myLabel = label(myTheme, "one");
742 
743     myLabel.io = io;
744     myLabel.draw();
745 
746     assert(myLabel.pickStyle().textColor == color!"100");
747     assert(myLabel.pickStyle().backgroundColor == color!"020");
748     assert(myLabel.style.backgroundColor == color!"aaa");
749 
750     myLabel.text = "";
751 
752     assert(myLabel.pickStyle().textColor == color!"200");
753     assert(myLabel.pickStyle().backgroundColor == color!"020");
754     assert(myLabel.style.backgroundColor == color!"aaa");
755 
756     myLabel.text = "two";
757 
758     assert(myLabel.pickStyle().textColor == color!"100");
759     assert(myLabel.pickStyle().backgroundColor == color!"010");
760     assert(myLabel.style.backgroundColor == color!"aaa");
761 
762 }
763 
764 struct StyleTemplate {
765 
766     alias fields = NoDuplicates!(getSymbolsByUDA!(Style, Style.Themable));
767 
768     // Create fields for every themable item
769     static foreach (field; fields) {
770 
771         static if (!isFunction!(typeof(field)))
772             mixin(q{ FieldValue!(typeof(field)) }, __traits(identifier, field), ";");
773 
774     }
775 
776     /// Update the given style using this template.
777     void apply(ref Style style) {
778 
779         // TODO only iterate on fields that have changed
780         static foreach (field; fields) {{
781 
782             auto newValue = mixin("this.", __traits(identifier, field));
783 
784             newValue.apply(__traits(child, style, field));
785 
786         }}
787 
788     }
789 
790     /// Combine with another style template, applying all local rules on the other template.
791     void apply(ref StyleTemplate style) {
792 
793         static foreach (i, field; fields) {
794 
795             this.tupleof[i].apply(style.tupleof[i]);
796 
797         }
798 
799     }
800 
801     string toString() const @trusted {
802 
803         string[] items;
804 
805         static foreach (field; fields) {{
806 
807             enum name = __traits(identifier, field);
808             auto value = mixin("this.", name);
809 
810             if (value.isSet)
811                 items ~= format!"%s: %s"(name, value);
812 
813         }}
814 
815         return format!"StyleTemplate(%-(%s, %))"(items);
816 
817     }
818 
819 }
820 
821 /// `Field` allows defining and performing partial changes to members of Style.
822 struct Field(string fieldName, T) {
823 
824     enum name = fieldName;
825     alias Type = T;
826 
827     FieldValue!T value;
828 
829     static Field make(Item, size_t n)(Item[n] value) {
830 
831         Field field;
832         field.value = value;
833         return field;
834 
835     }
836 
837     static Field make(T)(T value)
838     if (!isStaticArray!T)
839     do {
840 
841         Field field;
842         field.value = value;
843         return field;
844 
845     }
846 
847     static Field make() {
848 
849         return Field();
850 
851     }
852 
853     /// Apply on a style template.
854     void apply(ref StyleTemplate style) {
855 
856         value.apply(__traits(child, style, Rule.field!fieldName));
857 
858     }
859 
860     // Operators for structs
861     static if (is(T == struct)) {
862 
863         template opDispatch(string field)
864         if (__traits(hasMember, T, field))
865         {
866 
867             Field opDispatch(Input)(Input input) return {
868 
869                 __traits(getMember, value, field) = input;
870                 return this;
871 
872             }
873 
874         }
875 
876     }
877 
878     // Operators for arrays
879     static if (isStaticArray!T) {
880 
881         private size_t[2] slice;
882 
883         Field opAssign(Input, size_t n)(Input[n] input) return {
884 
885             value[slice] = input;
886             return this;
887 
888         }
889 
890         Field opAssign(Input)(Input input) return
891         if (!isStaticArray!Input)
892         do {
893 
894             value[slice] = input;
895             return this;
896 
897         }
898 
899         inout(Field) opIndex(size_t i) return inout {
900 
901             return inout Field(value, [i, i+1]);
902 
903         }
904 
905         inout(Field) opIndex(return inout Field slice) const {
906 
907             return slice;
908 
909         }
910 
911         inout(Field) opSlice(size_t dimension : 0)(size_t i, size_t j) return inout {
912 
913             return Field(value, [i, j]);
914 
915         }
916 
917     }
918 
919 }
920 
921 unittest {
922 
923     auto typeface = Style.loadTypeface(4);
924     auto sample = Rule.typeface = typeface;
925 
926     const originalTypeface = typeface;
927 
928     assert(sample.name == "typeface");
929 
930     auto target = Style.loadTypeface(3);
931     sample.value.apply(target);
932 
933     assert(target is typeface);
934     assert(typeface is originalTypeface);
935 
936 }
937 
938 unittest {
939 
940     auto sampleField = Rule.margin = 4;
941 
942     assert(sampleField.name == "margin");
943     assert(sampleField.slice == [0, 0]);
944 
945     float[4] field = [1, 1, 1, 1];
946     sampleField.value.apply(field);
947 
948     assert(field == [4, 4, 4, 4]);
949 
950 }
951 
952 unittest {
953 
954     auto sampleField = Rule.margin.sideX = 8;
955 
956     assert(sampleField.name == "margin");
957     assert(sampleField.slice == [0, 2]);
958 
959     float[4] field = [1, 1, 1, 1];
960     sampleField.value.apply(field);
961 
962     assert(field == [8, 8, 1, 1]);
963 
964 }
965 
966 template FieldValue(T) {
967 
968     // Struct
969     static if (is(T == struct))
970         alias FieldValue = FieldValueStruct!T;
971 
972     // Static array
973     else static if (is(T : E[n], E, size_t n))
974         alias FieldValue = FieldValueStaticArray!(E, n);
975 
976     // Others
977     else alias FieldValue = FieldValueOther!T;
978 
979 }
980 
981 private struct FieldValueStruct(T) {
982 
983     alias Type = T;
984     alias ExpandedType = staticMap!(FieldValue, typeof(Type.tupleof));
985 
986     ExpandedType value;
987     bool isSet;
988 
989     FieldValueStruct opAssign(FieldValueStruct value) {
990 
991         this.value = value.value;
992         this.isSet = value.isSet;
993 
994         return this;
995 
996     }
997 
998     Type opAssign(Type value) {
999 
1000         // Mark as set
1001         isSet = true;
1002 
1003         // Assign each field
1004         foreach (i, ref field; this.value) {
1005 
1006             field = value.tupleof[i];
1007 
1008         }
1009 
1010         return value;
1011 
1012     }
1013 
1014     template opDispatch(string name)
1015     if (__traits(hasMember, Type, name))
1016     {
1017 
1018         T opDispatch(T)(T value) {
1019 
1020             enum i = staticIndexOf!(name, FieldNameTuple!Type);
1021 
1022             // Mark as set
1023             isSet = true;
1024 
1025             // Assign the field
1026             this.value[i] = value;
1027 
1028             return value;
1029 
1030         }
1031 
1032     }
1033 
1034     /// Change the given value to match the requested change.
1035     void apply(ref Type value) {
1036 
1037         if (!isSet) return;
1038 
1039         foreach (i, field; this.value) {
1040             field.apply(value.tupleof[i]);
1041         }
1042 
1043     }
1044 
1045     /// Change the given value to match the requested change.
1046     void apply(ref FieldValueStruct value) {
1047 
1048         if (!isSet) return;
1049 
1050         value.isSet = true;
1051 
1052         foreach (i, field; this.value) {
1053             field.apply(value.value[i]);
1054         }
1055 
1056     }
1057 
1058 }
1059 
1060 private struct FieldValueStaticArray(E, size_t n) {
1061 
1062     alias Type = E[n];
1063     alias ExpandedType = FieldValue!E[n];
1064 
1065     ExpandedType value;
1066     bool isSet;
1067 
1068     FieldValueStaticArray opAssign(FieldValueStaticArray value) {
1069 
1070         this.value = value.value;
1071         this.isSet = value.isSet;
1072         return this;
1073 
1074     }
1075 
1076     Item[n] opAssign(Item, size_t n)(Item[n] value) {
1077 
1078         // Mark as changed
1079         isSet = true;
1080 
1081         // Assign each field
1082         foreach (i, ref field; this.value.tupleof) {
1083 
1084             field = value.tupleof[i];
1085 
1086         }
1087 
1088         return value;
1089 
1090     }
1091 
1092     Input opAssign(Input)(Input value)
1093     if (!isStaticArray!Input)
1094     do {
1095 
1096         // Implicit cast
1097         Type newValue = value;
1098 
1099         opAssign(newValue);
1100 
1101         return value;
1102 
1103     }
1104 
1105     Input opIndexAssign(Input)(Input input, size_t index) {
1106 
1107         isSet = true;
1108         value[index] = input;
1109         return input;
1110 
1111     }
1112 
1113     Input[n] opIndexAssign(Input, size_t n)(Input[n] input, size_t[2] indices) {
1114 
1115         assert(indices[1] - indices[0] == n, "Invalid slice");
1116 
1117         isSet = true;
1118         foreach (i, ref field; value[indices[0] .. indices[1]]) {
1119             field = input[i];
1120         }
1121 
1122         return input;
1123 
1124     }
1125 
1126     auto opIndexAssign(Input)(Input input, size_t[2] indices)
1127     if (!isStaticArray!Input)
1128     do {
1129 
1130         isSet = true;
1131         return value[indices[0] .. indices[1]] = FieldValue!E(input, true);
1132 
1133     }
1134 
1135     size_t[2] opSlice(size_t i, size_t j) const {
1136 
1137         return [i, j];
1138 
1139     }
1140 
1141     /// Change the given value to match the requested change.
1142     void apply(ref Type value) {
1143 
1144         if (!isSet) return;
1145 
1146         foreach (i, field; this.value.tupleof) {
1147             field.apply(value.tupleof[i]);
1148         }
1149 
1150     }
1151 
1152     /// Change the given value to match the requested change.
1153     void apply(ref FieldValueStaticArray value) {
1154 
1155         if (!isSet) return;
1156 
1157         value.isSet = true;
1158 
1159         foreach (i, field; this.value.tupleof) {
1160             field.apply(value.value[i]);
1161         }
1162 
1163     }
1164 
1165     string toString() {
1166 
1167         Type output;
1168         apply(output);
1169         return format!"%s"(output);
1170 
1171     }
1172 
1173 }
1174 
1175 private struct FieldValueOther(T) {
1176 
1177     alias Type = T;
1178 
1179     Type value;
1180     bool isSet;
1181 
1182     FieldValueOther opAssign(FieldValueOther value) {
1183 
1184         this.value = value.value;
1185         this.isSet = value.isSet;
1186 
1187         return this;
1188 
1189     }
1190 
1191     Type opAssign(Input)(Input value) {
1192 
1193         // Mark as set
1194         isSet = true;
1195 
1196         return this.value = value;
1197 
1198     }
1199 
1200     /// Change the given value to match the requested change.
1201     void apply(ref Type value) {
1202 
1203         if (!isSet) return;
1204 
1205         value = this.value;
1206 
1207     }
1208 
1209     /// Apply another modification to a field.
1210     void apply(ref FieldValueOther value) {
1211 
1212         if (!isSet) return;
1213 
1214         value.value = this.value;
1215         value.isSet = true;
1216 
1217     }
1218 
1219     string toString() const @trusted {
1220 
1221         return format!"%s"(value);
1222 
1223     }
1224 
1225 }