1 ///
2 module fluid.utils;
3 
4 import std.meta;
5 import std.traits;
6 import std.functional;
7 
8 import fluid.types;
9 
10 
11 @safe:
12 
13 
14 alias simpleConstructor = nodeBuilder;
15 alias SimpleConstructor = NodeBuilder;
16 alias isSimpleConstructor = isNodeBuilder;
17 
18 deprecated("Use NodeBuilder instead") {
19     alias componentBuilder = nodeBuilder;
20     alias ComponentBuilder = NodeBuilder;
21     alias isComponentBuilder = isNodeBuilder;
22 }
23 
24 // For saner testing and debugging.
25 version (unittest)
26 private extern(C) __gshared string[] rt_options = ["oncycle=ignore"];
27 
28 /// Create a component builder for declarative usage.
29 ///
30 /// Initial properties can be provided in the function provided in the second argument.
31 enum nodeBuilder(T, alias fun = "a") = NodeBuilder!(T, fun).init;
32 
33 /// Create a simple template node constructor for declarative usage.
34 ///
35 /// If the parent is a simple constructor, its initializer will be ran *after* this one. This is because the user
36 /// usually specifies the parent in templates, so it has more importance.
37 ///
38 /// T must be a template accepting a single parameter — Parent type will be passed to it.
39 template nodeBuilder(alias T, alias Parent, alias fun = "a") {
40 
41     alias nodeBuilder = nodeBuilder!(T!(Parent.Type), (a) {
42 
43         alias initializer = unaryFun!fun;
44 
45         initializer(a);
46         Parent.initialize(a);
47 
48     });
49 
50 }
51 
52 /// ditto
53 alias nodeBuilder(alias T, Parent, alias fun = "a") = nodeBuilder!(T!Parent, fun);
54 
55 enum isNodeBuilder(T) = is(T : NodeBuilder!(A, a), A, alias a);
56 
57 struct NodeBuilder(T, alias fun = "a") {
58 
59     import fluid.style;
60     import fluid.structs;
61 
62     alias Type = T;
63 
64     deprecated("`NodeBuilder.initializer` is affected by a codegen bug in DMD, "
65         ~ "and has been replaced with `initialize`. "
66         ~ "Please update your code before Fluid 0.8.0")
67     alias initializer = unaryFun!fun;
68 
69     void initialize(T node) {
70         unaryFun!fun(node);
71     }
72 
73     Type opCall(Args...)(Args args) {
74 
75         // Collect parameters
76         enum paramCount = leadingParams!Args;
77 
78         // Construct the node
79         auto result = new Type(args[paramCount..$]);
80 
81         // Run the initializer
82         initialize(result);
83 
84         // Apply the parameters
85         result.applyAll(args[0..paramCount]);
86 
87         return result;
88 
89     }
90 
91     /// Count node parameters present at the beginning of the given type list. This function is only available at
92     /// compile-time.
93     ///
94     /// If a node parameter is passed *after* a non-parameter, it will not be included in the count, and will not be
95     /// treated as one by ComponentBuilder.
96     static int leadingParams(Args...)() {
97 
98         assert(__ctfe, "leadingParams is not available at runtime");
99 
100         if (__ctfe)
101         foreach (i, Arg; Args) {
102 
103             // Found a non-parameter, return the index
104             if (!isNodeParam!(Arg, T))
105                 return i;
106 
107         }
108 
109         // All arguments are parameters
110         return Args.length;
111 
112     }
113 
114 }
115 
116 unittest {
117 
118     static class Foo {
119 
120         string value;
121 
122         this() { }
123 
124     }
125 
126     alias xfoo = nodeBuilder!Foo;
127     assert(xfoo().value == "");
128 
129     alias yfoo = nodeBuilder!(Foo, (a) {
130         a.value = "foo";
131     });
132     assert(yfoo().value == "foo");
133 
134     auto myFoo = new Foo;
135     yfoo.initialize(myFoo);
136     assert(myFoo.value == "foo");
137 
138     static class Bar(T) : T {
139 
140         int foo;
141 
142         this(int foo) {
143 
144             this.foo = foo;
145 
146         }
147 
148     }
149 
150     alias xbar(alias T) = nodeBuilder!(Bar, T);
151 
152     const barA = xbar!Foo(1);
153     assert(barA.value == "");
154     assert(barA.foo == 1);
155 
156     const barB = xbar!xfoo(2);
157     assert(barB.value == "");
158     assert(barB.foo == 2);
159 
160     const barC = xbar!yfoo(3);
161     assert(barC.value == "foo");
162     assert(barC.foo == 3);
163 
164 }
165 
166 /// Modify the subject by passing it to the `apply` method of each of the parameters.
167 ///
168 /// This is made for `nodeBuilder` to apply node parameters on a node. The subject doesn't have to be a node.
169 ///
170 /// Params:
171 ///     subject    = Subject to modify.
172 ///     parameters = Parameters to apply onto the subject;
173 /// Returns:
174 ///     The subject after applying the modifications.
175 ///     If subject is a class, this is the same object as passed.
176 Subject applyAll(Subject, Parameters...)(Subject subject, Parameters parameters) {
177 
178     foreach (param; parameters) {
179 
180         param.apply(subject);
181 
182     }
183 
184     return subject;
185 
186 }
187 
188 /// Get distance between two vectors.
189 float distance(Vector2 a, Vector2 b) {
190 
191     import std.math : sqrt;
192 
193     return sqrt(distance2(a, b));
194 
195 }
196 
197 /// Get distance between two vectors, squared.
198 float distance2(Vector2 a, Vector2 b) {
199 
200     return (a.x - b.x)^^2 + (a.y - b.y)^^2;
201 
202 }
203 
204 /// Convert points to pixels.
205 /// Params:
206 ///     points = Input value in points.
207 /// Returns: Given value in pixels.
208 float pt(float points) {
209 
210     // 1 pt = 1/72 in
211     // 1 px = 1/96 in
212     // 96 px = 72 pt
213 
214     return points * 96 / 72;
215 
216 }
217 
218 /// Convert pixels to points.
219 /// Params:
220 ///     points = Input value in pixels.
221 /// Returns: Given value in points.
222 float pxToPt(float px) {
223 
224     return px * 72 / 96;
225 
226 }
227 
228 unittest {
229 
230     import std.conv;
231 
232     assert(to!int(4.pt * 100) == 533);
233     assert(to!int(5.33.pxToPt * 100) == 399);
234 
235 
236 }
237 
238 /// Check if the rectangle contains a point.
239 bool contains(Rectangle rectangle, Vector2 point) {
240 
241     return rectangle.x <= point.x
242         && point.x < rectangle.x + rectangle.width
243         && rectangle.y <= point.y
244         && point.y < rectangle.y + rectangle.height;
245 
246 }
247 
248 /// Check if the two rectangles overlap.
249 bool overlap(Rectangle a, Rectangle b) {
250 
251     const x = (start(b).x <= a.x && a.x <= end(b).x)
252         ||    (start(a).x <= b.x && b.x <= end(a).x);
253     const y = (start(b).y <= a.y && a.y <= end(b).y)
254         ||    (start(a).y <= b.y && b.y <= end(a).y);
255 
256     return x && y;
257 
258 }
259 
260 /// Load a two dimensional vector from a string.
261 ///
262 /// The string should either be a single float value, like `1.5`, or two, separated by an `x`
263 /// character: `1.5 x 1.2`. If there is only one value, it will be used for both axes.
264 ///
265 /// Params:
266 ///     source = String to parse.
267 /// Params:
268 ///     String to load from.
269 Vector2 toSizeVector2(string source) {
270 
271     import std.conv : to;
272     import std.string : strip;
273     import std.algorithm : findSplit;
274 
275     // Load the render scale from environment
276     if (auto pair = source.findSplit("x")) {
277         return Vector2(
278             pair[0].strip.to!float,
279             pair[2].strip.to!float
280         );
281     }
282     else {
283         const value = source.strip.to!float;
284         return Vector2(value, value);
285     }
286 
287 }
288 
289 unittest {
290 
291     assert("1.5".toSizeVector2 == Vector2(1.5, 1.5));
292     assert("1.5x1.2".toSizeVector2 == Vector2(1.5, 1.2));
293     assert("2.0 x 1.0".toSizeVector2 == Vector2(2.0, 1.0));
294     assert(" 2.0x1.0 ".toSizeVector2 == Vector2(2.0, 1.0));
295 
296 }
297 
298 // Extremely useful Rectangle utilities
299 
300 /// Get the top-left corner of a rectangle.
301 Vector2 start(Rectangle r) nothrow {
302     return Vector2(r.x, r.y);
303 }
304 
305 /// Get the bottom-right corner of a rectangle.
306 Vector2 end(Rectangle r) nothrow {
307     return Vector2(r.x + r.w, r.y + r.h);
308 }
309 
310 /// Get the center of a rectangle.
311 Vector2 center(Rectangle r) nothrow {
312     return Vector2(r.x + r.w/2, r.y + r.h/2);
313 }
314 
315 /// Get the size of a rectangle.
316 Vector2 size(Rectangle r) nothrow {
317     return Vector2(r.w, r.h);
318 }
319 
320 /// Get the area of a rectangle.
321 float area(Rectangle r) nothrow {
322     return r.w * r.h;
323 }
324 
325 /// Intersect two rectangles
326 Rectangle intersect(Rectangle one, Rectangle two) nothrow {
327 
328     import std.algorithm : min, max;
329 
330     Rectangle result;
331     result.x = max(one.x, two.x);
332     result.y = max(one.y, two.y);
333     result.w = max(0, min(one.x + one.w, two.x + two.w) - result.x);
334     result.h = max(0, min(one.y + one.h, two.y + two.h) - result.y);
335     return result;
336 
337 }
338 
339 /// Create a point that is in the same position relative to the destination rectangle,
340 /// as is the input point relative to the source rectangle.
341 ///
342 /// Relation is expressed is in term of a fraction or percentage. If the point is in the center
343 /// of the source rectangle, the returned point will be in the center of the destination
344 /// rectangle.
345 ///
346 /// Params:
347 ///     point       = Point to transform.
348 ///     source      = Original viewport; source point is relative to this viewport.
349 ///     destination = Viewport used as destination. Resulting point will be relative
350 ///         to this viewport.
351 /// Returns:
352 ///     A point located in the same place, relative to the other viewport.
353 Vector2 viewportTransform(Vector2 point, Rectangle source, Rectangle destination) {
354     point = point - source.start;
355     point = Vector2(
356         point.x * destination.width  / source.width,
357         point.y * destination.height / source.height,
358     );
359     return point + destination.start;
360 }
361 
362 ///
363 @("Viewport transform example works")
364 unittest {
365 
366     const source      = Rectangle(100, 100,  50,  50);
367     const destination = Rectangle(100,   0, 100, 100);
368 
369     // Corners and center
370     assert(source.start .viewportTransform(source, destination) == destination.start);
371     assert(source.center.viewportTransform(source, destination) == destination.center);
372     assert(source.end   .viewportTransform(source, destination) == destination.end);
373 
374     // Arbitrary positions
375     assert(Vector2(125, 100).viewportTransform(source, destination) == Vector2( 150,    0));
376     assert(Vector2(  0,   0).viewportTransform(source, destination) == Vector2(-100, -200));
377 
378 }
379 
380 /// Get names of static fields in the given object.
381 ///
382 /// Ignores deprecated fields.
383 template StaticFieldNames(T) {
384 
385     import std.traits : hasStaticMember;
386     import std.meta : Alias, Filter;
387 
388     // Prepare data
389     alias Members = __traits(allMembers, T);
390 
391     template isStaticMember(string member) {
392 
393         enum isStaticMember =
394 
395             // Make sure this isn't an alias
396             __traits(compiles,
397                 Alias!(__traits(getMember, T, member))
398             )
399 
400             && !__traits(isDeprecated, __traits(getMember, T, member))
401 
402             // Find the member
403             && hasStaticMember!(T, member);
404 
405     }
406 
407     // Result
408     alias StaticFieldNames = Filter!(isStaticMember, Members);
409 
410 }
411 
412 /// Open given URL in a web browser.
413 ///
414 /// Supports all major desktop operating systems. Does nothing if not supported on the given platform.
415 ///
416 /// At the moment this simply wraps `std.process.browse`.
417 void openURL(scope const(char)[] url) nothrow {
418 
419     version (Posix) {
420         import std.process;
421         browse(url);
422     }
423     else version (Windows) {
424         import std.process;
425         browse(url);
426     }
427 
428     // Do nothing on remaining platforms
429 
430 }