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 @("Legacy: Themes apply to nodes (migrated)")
138 unittest {
139 
140     import fluid.label;
141 
142     Theme theme;
143 
144     with (Rule)
145     theme.add(
146         rule!Label(
147             textColor = color!"#abc",
148         ),
149     );
150 
151     auto io = new HeadlessBackend;
152     auto root = label(theme, "placeholder");
153     root.io = io;
154 
155     root.draw();
156     io.assertTexture(root.text.texture.chunks[0], Vector2(0, 0), color!"#fff");
157     assert(root.text.texture.chunks[0].palette[0] == color("#abc"));
158 
159 }
160 
161 // Define field setters for each field, to use by importing or through `Rule.property = value`
162 static foreach (field; StyleTemplate.fields) {
163 
164     mixin(
165         `alias `,
166         __traits(identifier, field),
167         `= Field!("`,
168         __traits(identifier, field),
169         `", typeof(field)).make;`
170     );
171 
172 }
173 
174 // Separately define opacity for convenience
175 auto opacity(float value) {
176 
177     import std.algorithm : clamp;
178 
179     return tint.a = cast(ubyte) clamp(value * ubyte.max, ubyte.min, ubyte.max);
180 
181 }
182 
183 /// Selector is used to pick a node based on its type and specified tags.
184 struct Selector {
185 
186     /// Type of the node to match.
187     TypeInfo_Class type;
188 
189     /// Tags needed by the selector.
190     TagList tags;
191 
192     /// If true, this selector will reject any match.
193     bool rejectAll;
194 
195     /// Returns a selector that doesn't match anything
196     static Selector none() {
197 
198         Selector result;
199         result.rejectAll = true;
200         return result;
201 
202     }
203 
204     /// Test if the selector matches given node.
205     bool test(Node node) {
206 
207         return !rejectAll
208             && testType(typeid(node))
209             && testTags(node.tags);
210 
211     }
212 
213     /// Test if the given type matches the selector.
214     bool testType(TypeInfo_Class type) {
215 
216         return !this.type || this.type.isBaseOf(type);
217 
218     }
219 
220     @("Selector.testType correctly identifies node types")
221     unittest {
222 
223         import fluid.input;
224         import fluid.label;
225         import fluid.button;
226 
227         auto myLabel = label("my label");
228         auto myButton = button("my button", delegate { });
229 
230         auto selector = Selector(typeid(Node));
231 
232         assert(selector.test(myLabel));
233         assert(selector.test(myButton));
234 
235         auto anotherSelector = Selector(typeid(Button));
236 
237         assert(!anotherSelector.test(myLabel));
238         assert(anotherSelector.test(myButton));
239 
240         auto noSelector = Selector();
241 
242         assert(noSelector.test(myLabel));
243         assert(noSelector.test(myButton));
244 
245     }
246 
247     /// True if all tags in this selector are present on the given node.
248     bool testTags(TagList tags) {
249 
250         // TODO linear search if there's only one tag
251         return this.tags.intersect(tags).walkLength == this.tags.length;
252 
253     }
254 
255     @("Selector.testTags correctly identifies tags")
256     unittest {
257 
258         import fluid.label;
259 
260         @NodeTag enum Good;
261         @NodeTag enum Bad;
262 
263         auto selector = Selector(typeid(Label)).addTags!Good;
264 
265         assert(selector.test(label(.tags!Good, "")));
266         assert(!selector.test(label(.tags!Bad, "")));
267         assert(!selector.test(label("")));
268 
269     }
270 
271     /// Create a new selector requiring the given set of tags.
272     Selector addTags(tags...)() {
273 
274         return Selector(type, this.tags.add!tags);
275 
276     }
277 
278 }
279 
280 /// Create a style rule for the given node.
281 ///
282 /// Template parameters are used to select the node the rule applies to, based on its type and the tags it has. Regular
283 /// parameters define the changes made by the rule. These are created by using automatically defined members of `Rule`,
284 /// which match names of `Style` fields. For example, to change the property `Style.textColor`, one would assign
285 /// `Rule.textColor` inside this parameter list.
286 ///
287 /// ---
288 /// rule!Label(
289 ///     Rule.textColor = color("#fff"),
290 ///     Rule.backgroundColor = color("#000"),
291 /// )
292 /// ---
293 ///
294 /// It is also possible to pass a `when` subrule to apply changes based on runtime conditions:
295 ///
296 /// ---
297 /// rule!Button(
298 ///     Rule.backgroundColor = color("#fff"),
299 ///     when!"a.isHovered"(
300 ///         Rule.backgroundColor = color("#ccc"),
301 ///     )
302 /// )
303 /// ---
304 ///
305 /// If some directives are repeated across different rules, they can be reused:
306 ///
307 /// ---
308 /// myRule = rule(
309 ///     Rule.textColor = color("#000"),
310 /// ),
311 /// rule!Button(
312 ///     myRule,
313 ///     Rule.backgroundColor = color("#fff"),
314 /// )
315 /// ---
316 ///
317 /// Moreover, rules respect inheritance. Since `Button` derives from `Label` and `Node`, `rule!Label` will also apply
318 /// to buttons, and so will `rule!Node`.
319 ///
320 /// For more advanced use-cases, it is possible to directly pass a delegate that accepts a node and returns a
321 /// subrule.
322 ///
323 /// ---
324 /// rule!Button(
325 ///     a => rule(
326 ///         Rule.backgroundColor = pickColor(),
327 ///     )
328 /// )
329 /// ---
330 ///
331 /// It is recommended to use the `with (Rule)` statement to make rule definitions clearer.
332 template rule(T : Node = Node, tags...) {
333 
334     Rule rule(Ts...)(Ts fields) {
335 
336         enum isWhenRule(alias field) = is(typeof(field) : WhenRule!dg, alias dg);
337         enum isDynamicRule(alias field) = isCallable!field || isWhenRule!field || is(typeof(field) : Rule);
338 
339         Rule result;
340         Rule[] crumbs;
341 
342         // Create the selector
343         result.selector = Selector(typeid(T)).addTags!tags;
344 
345         // Load fields
346         static foreach (i, field; fields) {{
347 
348             // Directly assigned field
349             static if (is(typeof(field) : Field!(fieldName, T), string fieldName, T)) {
350 
351                 // Add to the result
352                 field.apply(result.fields);
353 
354             }
355 
356             // Copy from another rule
357             else static if (is(typeof(field) : Rule)) {
358 
359                 assert(field.selector.testType(typeid(T)),
360                     format!"Cannot paste rule for %s into a rule for %s"(field.selector.type, typeid(T)));
361 
362                 // Copy fields
363                 field.fields.apply(result.fields);
364 
365                 // Merge breadcrumbs
366                 result.breadcrumbs.crumbs ~= field.breadcrumbs.crumbs;
367 
368                 // Also add delegates below...
369 
370             }
371 
372             // Children rule
373             else static if (is(typeof(field) : ChildrenRule)) {
374 
375                 // Insert the rule into breadcrumbs
376                 crumbs ~= field.rule;
377 
378             }
379 
380             // Dynamic rule
381             else static if (isDynamicRule!field) { }
382 
383             else static assert(false, format!"Unrecognized type %s (argument index %s)"(typeof(field).stringof, i));
384 
385         }}
386 
387         // Load delegates
388         alias delegates = Filter!(isDynamicRule, fields);
389 
390         // Build the dynamic rule delegate
391         static if (delegates.length)
392         result.styleDelegate = (Node node) {
393 
394             Rule dynamicResult;
395 
396             // Cast the node into proper type
397             auto castNode = cast(T) node;
398             assert(castNode, "styleDelegate was passed an invalid node");
399 
400             static foreach (dg; delegates) {
401 
402                 // A "when" rule
403                 static if (isWhenRule!dg) {
404 
405                     // Test the predicate before applying the result
406                     if (dg.predicate(castNode)) {
407 
408                         dg.rule.apply(node, dynamicResult);
409 
410                     }
411 
412                     // Apply the alternative if predicate fails
413                     else dg.alternativeRule.apply(node, dynamicResult);
414 
415                 }
416 
417                 // Regular rule, forward to its delegate
418                 else static if (is(typeof(dg) : Rule)) {
419 
420                     if (dg.styleDelegate)
421                     dg.styleDelegate(node).apply(node, dynamicResult);
422 
423                 }
424 
425                 // Use the delegate and apply the result on the template of choice
426                 else dg(castNode).apply(node, dynamicResult);
427 
428             }
429 
430             return dynamicResult;
431 
432         };
433 
434         // Append ruleset from breadcrumbs to current breadcrumbs
435         if (crumbs) {
436             result.breadcrumbs.crumbs ~= crumbs;
437         }
438 
439         return result;
440 
441     }
442 
443 }
444 
445 /// Rules specify changes that are to be made to the node's style.
446 struct Rule {
447 
448     alias StyleDelegate = Rule delegate(Node node) @safe;
449     alias loadTypeface = Style.loadTypeface;
450 
451     public import fluid.theme;
452 
453     /// Selector to filter items that should match this rule.
454     Selector selector;
455 
456     /// Fields affected by this rule and their values.
457     StyleTemplate fields;
458 
459     /// Callback for updating the style dynamically. May be null.
460     StyleDelegate styleDelegate;
461 
462     /// Breadcrumbs, if any, assigned to nodes matching this rule.
463     Breadcrumbs breadcrumbs;
464 
465     alias field(string name) = __traits(getMember, StyleTemplate, name);
466     alias FieldType(string name) = field!name.Type;
467 
468     /// Returns true if the rule can be applied to the given node.
469     bool canApply(Node node) {
470 
471         return selector.test(node);
472 
473     }
474 
475     /// Combine with another rule. Applies dynamic rules immediately.
476     bool apply(Node node, ref Rule rule) {
477 
478         // Test against the selector
479         if (!canApply(node)) return false;
480 
481         // Apply changes
482         fields.apply(rule.fields);
483 
484         // Load breadcrumbs
485         rule.breadcrumbs ~= breadcrumbs;
486 
487         // Check for delegates
488         if (styleDelegate) {
489 
490             // Run and apply the delegate
491             styleDelegate(node).apply(node, rule);
492 
493         }
494 
495         return true;
496 
497     }
498 
499     /// Apply this rule on the given style.
500     /// Returns: True if applied, false if not.
501     bool apply(Node node, ref Style style) {
502 
503         // Apply the rule
504         if (!applyStatic(node, style)) return false;
505 
506         // Check for delegates
507         if (styleDelegate) {
508 
509             // Run and apply the delegate
510             styleDelegate(node).apply(node, style);
511 
512         }
513 
514         return true;
515 
516     }
517 
518     /// Apply this rule on the given style. Ignores dynamic styles.
519     /// Returns: True if applied, false if not.
520     bool applyStatic(Node node, ref Style style) {
521 
522         // Test against the selector
523         if (!canApply(node)) return false;
524 
525         // Apply changes
526         fields.apply(style);
527 
528         // Load breadcrumbs
529         style.breadcrumbs ~= breadcrumbs;
530 
531         return true;
532 
533     }
534 
535 }
536 
537 /// Branch out in a rule to apply styling based on a runtime condition.
538 WhenRule!predicate when(alias predicate, Args...)(Args args) {
539 
540     return WhenRule!predicate(rule(args));
541 
542 }
543 
544 struct WhenRule(alias dg) {
545 
546     import std.functional;
547 
548     /// Function to evaluate to test if the rule should be applied.
549     alias predicate = unaryFun!dg;
550 
551     /// Rule to apply.
552     Rule rule;
553 
554     /// Rule to apply when the predicate fails.
555     Rule alternativeRule;
556 
557     /// Specify rule to apply in case the predicate fails. An `else` branch.
558     WhenRule otherwise(Args...)(Args args) {
559 
560         // TODO else if?
561 
562         auto result = this;
563         result.alternativeRule = .rule(args);
564         return result;
565 
566     }
567 
568 }
569 
570 /// Create a rule that affects the children of a node. To be placed inside a regular rule.
571 ///
572 /// A `children` rule creates a "breadcrumb" which is a tag applied to the node that tracks
573 /// all `children` rules affecting it, including all `children` rules it has spawned. Every node will
574 /// then activate corresponding rules
575 template children(T : Node = Node, tags...) {
576 
577     ChildrenRule children(Ts...)(Ts fields) {
578 
579         return ChildrenRule(rule!(T, tags)(fields));
580 
581     }
582 
583 }
584 
585 /// A version of `Rule` that affects children.
586 struct ChildrenRule {
587 
588     Rule rule;
589 
590 }
591 
592 struct Breadcrumbs {
593 
594     alias Key = size_t;
595 
596     /// All rules activated by this instance.
597     Rule[][] crumbs;
598 
599     /// Cached children instances.
600     Breadcrumbs[Key] children;
601 
602     bool opCast(T : bool)() const {
603 
604         return this !is this.init;
605 
606     }
607 
608     /// Get an key for the given ruleset.
609     static Key key(Rule[] rules) {
610 
611         return cast(Key) rules.ptr;
612 
613     }
614 
615     /// Apply the breadcrumbs on the given node. Runs static rules only.
616     void applyStatic(Node node, ref Style style) {
617 
618         foreach (rules; crumbs) {
619 
620             foreach (rule; rules) {
621 
622                 // Apply the styles
623                 // applyStatic tests compatibility automatically
624                 rule.applyStatic(node, style);
625 
626             }
627 
628         }
629 
630     }
631 
632     /// Apply the breadcrumbs on the given node. Runs dynamic rules only.
633     void applyDynamic(Node node, ref Style style) {
634 
635         foreach (rules; crumbs) {
636 
637             foreach (rule; rules) {
638 
639                 if (rule.styleDelegate && rule.canApply(node)) {
640 
641                     rule.styleDelegate(node).apply(node, style);
642 
643                 }
644 
645             }
646 
647         }
648 
649     }
650 
651     /// Combine with another breadcrumbs instance.
652     ///
653     /// This breadcrumb will now point to the same breadcrumb as the one given, but the chain will be combined to
654     /// include both of them.
655     ref Breadcrumbs opOpAssign(string op : "~")(Breadcrumbs other) return {
656 
657         // Stop if there's nothing to do
658         if (!other) return this;
659 
660         foreach (rules; other.crumbs) {
661 
662             // Skip empty crumbs
663             if (rules.length == 0) continue;
664 
665             // Loop up the entry in the cache
666             // If one isn't present, create a new one with the ruleset appended
667             this = children.require(key(rules), Breadcrumbs(crumbs ~ rules));
668 
669         }
670 
671         return this;
672 
673     }
674 
675 }
676 
677 struct StyleTemplate {
678 
679     alias fields = NoDuplicates!(getSymbolsByUDA!(Style, Style.Themable));
680 
681     // Create fields for every themable item
682     static foreach (field; fields) {
683 
684         static if (!isFunction!(typeof(field)))
685             mixin(q{ FieldValue!(typeof(field)) }, __traits(identifier, field), ";");
686 
687     }
688 
689     /// Update the given style using this template.
690     void apply(ref Style style) {
691 
692         // TODO only iterate on fields that have changed
693         static foreach (field; fields) {{
694 
695             auto newValue = mixin("this.", __traits(identifier, field));
696 
697             newValue.apply(__traits(child, style, field));
698 
699         }}
700 
701     }
702 
703     /// Combine with another style template, applying all local rules on the other template.
704     void apply(ref StyleTemplate style) {
705 
706         static foreach (i, field; fields) {
707 
708             this.tupleof[i].apply(style.tupleof[i]);
709 
710         }
711 
712     }
713 
714     string toString()() const @trusted {
715 
716         string[] items;
717 
718         static foreach (field; fields) {{
719 
720             enum name = __traits(identifier, field);
721             auto value = mixin("this.", name);
722 
723             if (value.isSet)
724                 items ~= format("%s: %s", name, value);
725 
726         }}
727 
728         return format("StyleTemplate(%-(%s, %))", items);
729 
730     }
731 
732 }
733 
734 /// `Field` allows defining and performing partial changes to members of Style.
735 struct Field(string fieldName, T) {
736 
737     enum name = fieldName;
738     alias Type = T;
739 
740     FieldValue!T value;
741 
742     static Field make(Item, size_t n)(Item[n] value) {
743 
744         Field field;
745         field.value = value;
746         return field;
747 
748     }
749 
750     static Field make(T)(T value)
751     if (!isStaticArray!T)
752     do {
753 
754         Field field;
755         field.value = value;
756         return field;
757 
758     }
759 
760     static Field make() {
761 
762         return Field();
763 
764     }
765 
766     /// Apply on a style template.
767     void apply(ref StyleTemplate style) {
768 
769         value.apply(__traits(child, style, Rule.field!fieldName));
770 
771     }
772 
773     // Operators for structs
774     static if (is(T == struct)) {
775 
776         template opDispatch(string field)
777         if (__traits(hasMember, T, field))
778         {
779 
780             Field opDispatch(Input)(Input input) return {
781 
782                 __traits(getMember, value, field) = input;
783                 return this;
784 
785             }
786 
787         }
788 
789     }
790 
791     // Operators for arrays
792     static if (isStaticArray!T) {
793 
794         private size_t[2] slice;
795 
796         Field opAssign(Input, size_t n)(Input[n] input) return {
797 
798             value[slice] = input;
799             return this;
800 
801         }
802 
803         Field opAssign(Input)(Input input) return
804         if (!isStaticArray!Input)
805         do {
806 
807             value[slice] = input;
808             return this;
809 
810         }
811 
812         inout(Field) opIndex(size_t i) return inout {
813 
814             return inout Field(value, [i, i+1]);
815 
816         }
817 
818         inout(Field) opIndex(return inout Field slice) const {
819 
820             return slice;
821 
822         }
823 
824         inout(Field) opSlice(size_t dimension : 0)(size_t i, size_t j) return inout {
825 
826             return Field(value, [i, j]);
827 
828         }
829 
830     }
831 
832 }
833 
834 template FieldValue(T) {
835 
836     // Struct
837     static if (is(T == struct))
838         alias FieldValue = FieldValueStruct!T;
839 
840     // Static array
841     else static if (is(T : E[n], E, size_t n))
842         alias FieldValue = FieldValueStaticArray!(E, n);
843 
844     // Others
845     else alias FieldValue = FieldValueOther!T;
846 
847 }
848 
849 private struct FieldValueStruct(T) {
850 
851     alias Type = T;
852     alias ExpandedType = staticMap!(FieldValue, typeof(Type.tupleof));
853 
854     ExpandedType value;
855     bool isSet;
856 
857     FieldValueStruct opAssign(FieldValueStruct value) {
858 
859         this.value = value.value;
860         this.isSet = value.isSet;
861 
862         return this;
863 
864     }
865 
866     Type opAssign(Type value) {
867 
868         // Mark as set
869         isSet = true;
870 
871         // Assign each field
872         foreach (i, ref field; this.value) {
873 
874             field = value.tupleof[i];
875 
876         }
877 
878         return value;
879 
880     }
881 
882     template opDispatch(string name)
883     if (__traits(hasMember, Type, name))
884     {
885 
886         T opDispatch(T)(T value) {
887 
888             enum i = staticIndexOf!(name, FieldNameTuple!Type);
889 
890             // Mark as set
891             isSet = true;
892 
893             // Assign the field
894             this.value[i] = value;
895 
896             return value;
897 
898         }
899 
900     }
901 
902     /// Change the given value to match the requested change.
903     void apply(ref Type value) {
904 
905         if (!isSet) return;
906 
907         foreach (i, field; this.value) {
908             field.apply(value.tupleof[i]);
909         }
910 
911     }
912 
913     /// Change the given value to match the requested change.
914     void apply(ref FieldValueStruct value) {
915 
916         if (!isSet) return;
917 
918         value.isSet = true;
919 
920         foreach (i, field; this.value) {
921             field.apply(value.value[i]);
922         }
923 
924     }
925 
926 }
927 
928 private struct FieldValueStaticArray(E, size_t n) {
929 
930     alias Type = E[n];
931     alias ExpandedType = FieldValue!E[n];
932 
933     ExpandedType value;
934     bool isSet;
935 
936     FieldValueStaticArray opAssign(FieldValueStaticArray value) {
937 
938         this.value = value.value;
939         this.isSet = value.isSet;
940         return this;
941 
942     }
943 
944     Item[n] opAssign(Item, size_t n)(Item[n] value) {
945 
946         // Mark as changed
947         isSet = true;
948 
949         // Assign each field
950         foreach (i, ref field; this.value[]) {
951 
952             field = value[i];
953 
954         }
955 
956         return value;
957 
958     }
959 
960     Input opAssign(Input)(Input value)
961     if (!isStaticArray!Input)
962     do {
963 
964         // Implicit cast
965         Type newValue = value;
966 
967         opAssign(newValue);
968 
969         return value;
970 
971     }
972 
973     Input opIndexAssign(Input)(Input input, size_t index) {
974 
975         isSet = true;
976         value[index] = input;
977         return input;
978 
979     }
980 
981     Input[n] opIndexAssign(Input, size_t n)(Input[n] input, size_t[2] indices) {
982 
983         assert(indices[1] - indices[0] == n, "Invalid slice");
984 
985         isSet = true;
986         foreach (i, ref field; value[indices[0] .. indices[1]]) {
987             field = input[i];
988         }
989 
990         return input;
991 
992     }
993 
994     auto opIndexAssign(Input)(Input input, size_t[2] indices)
995     if (!isStaticArray!Input)
996     do {
997 
998         isSet = true;
999         return value[indices[0] .. indices[1]] = FieldValue!E(input, true);
1000 
1001     }
1002 
1003     size_t[2] opSlice(size_t i, size_t j) const {
1004 
1005         return [i, j];
1006 
1007     }
1008 
1009     /// Change the given value to match the requested change.
1010     void apply(ref Type value) {
1011 
1012         if (!isSet) return;
1013 
1014         foreach (i, field; this.value[]) {
1015             field.apply(value[i]);
1016         }
1017 
1018     }
1019 
1020     /// Change the given value to match the requested change.
1021     void apply(ref FieldValueStaticArray value) {
1022 
1023         if (!isSet) return;
1024 
1025         value.isSet = true;
1026 
1027         foreach (i, field; this.value[]) {
1028             field.apply(value.value[i]);
1029         }
1030 
1031     }
1032 
1033     string toString() {
1034 
1035         Type output;
1036         apply(output);
1037         return format!"%s"(output);
1038 
1039     }
1040 
1041 }
1042 
1043 private struct FieldValueOther(T) {
1044 
1045     alias Type = T;
1046 
1047     Type value;
1048     bool isSet;
1049 
1050     FieldValueOther opAssign(FieldValueOther value) {
1051 
1052         this.value = value.value;
1053         this.isSet = value.isSet;
1054 
1055         return this;
1056 
1057     }
1058 
1059     Type opAssign(Input)(Input value) {
1060 
1061         // Mark as set
1062         isSet = true;
1063 
1064         return this.value = value;
1065 
1066     }
1067 
1068     /// Change the given value to match the requested change.
1069     void apply(ref Type value) {
1070 
1071         if (!isSet) return;
1072 
1073         value = this.value;
1074 
1075     }
1076 
1077     /// Apply another modification to a field.
1078     void apply(ref FieldValueOther value) {
1079 
1080         if (!isSet) return;
1081 
1082         value.value = this.value;
1083         value.isSet = true;
1084 
1085     }
1086 
1087     string toString() const @trusted {
1088 
1089         return format!"%s"(value);
1090 
1091     }
1092 
1093 }