1 ///
2 module fluid.style;
3 
4 import std.math;
5 import std.range;
6 import std.format;
7 import std.typecons;
8 import std.algorithm;
9 
10 import fluid.node;
11 import fluid.utils;
12 import fluid.backend;
13 import fluid.typeface;
14 
15 public import fluid.theme : makeTheme, Theme, Selector, rule, Rule, when, WhenRule, children, ChildrenRule, Field, 
16     Breadcrumbs;
17 public import fluid.border;
18 public import fluid.default_theme;
19 public import fluid.backend : color;
20 
21 
22 @safe:
23 
24 
25 /// Contains the style for a node.
26 struct Style {
27 
28     enum Themable;
29 
30     enum Side {
31 
32         left, right, top, bottom,
33 
34     }
35 
36     // Text options
37     @Themable {
38 
39         /// Main typeface to be used for text.
40         ///
41         /// Changing the typeface requires a resize.
42         Typeface typeface;
43 
44         alias font = typeface;
45 
46         /// Text color.
47         Color textColor;
48 
49     }
50 
51     // Background & content
52     @Themable {
53 
54         /// Color of lines belonging to the node, especially important to separators and sliders.
55         Color lineColor;
56 
57         /// Background color of the node.
58         Color backgroundColor;
59 
60         /// Background color for selected text.
61         Color selectionBackgroundColor;
62 
63     }
64 
65     // Spacing
66     @Themable {
67 
68         /// Margin (outer margin) of the node. `[left, right, top, bottom]`.
69         ///
70         /// Updating margins requires a resize.
71         ///
72         /// See: `isSideArray`.
73         float[4] margin = 0;
74 
75         /// Border size, placed between margin and padding. `[left, right, top, bottom]`.
76         ///
77         /// Updating border requires a resize.
78         ///
79         /// See: `isSideArray`
80         float[4] border = 0;
81 
82         /// Padding (inner margin) of the node. `[left, right, top, bottom]`.
83         ///
84         /// Updating padding requires a resize.
85         ///
86         /// See: `isSideArray`
87         float[4] padding = 0;
88 
89         /// Margin/gap between two neighboring elements; for container nodes that support it.
90         ///
91         /// Updating the gap requires a resize.
92         float[2] gap = 0;
93 
94         /// Border style to use.
95         ///
96         /// Updating border requires a resize.
97         FluidBorder borderStyle;
98 
99     }
100 
101     // Misc
102     public {
103 
104         /// Apply tint to all node contents, including children.
105         @Themable
106         Color tint = Color(0xff, 0xff, 0xff, 0xff);
107 
108         /// Cursor icon to use while this node is hovered.
109         ///
110         /// Custom image cursors are not supported yet.
111         @Themable
112         FluidMouseCursor mouseCursor;
113 
114         /// Additional information for the node the style applies to.
115         ///
116         /// Ignored if mismatched.
117         @Themable
118         Node.Extra extra;
119 
120         /// Get or set node opacity. Value in range [0, 1] — 0 is fully transparent, 1 is fully opaque.
121         float opacity() const {
122 
123             return tint.a / 255.0f;
124 
125         }
126 
127         /// ditto
128         float opacity(float value) {
129 
130             tint.a = cast(ubyte) clamp(value * ubyte.max, ubyte.min, ubyte.max);
131 
132             return value;
133 
134         }
135 
136     }
137 
138     public {
139 
140         /// Breadcrumbs associated with this style. Used to keep track of tree-aware theme selectors, such as 
141         /// `children`. Does not include breadcrumbs loaded by parent nodes.
142         Breadcrumbs breadcrumbs;
143 
144     }
145 
146     private this(Typeface typeface) {
147 
148         this.typeface = typeface;
149 
150     }
151 
152     static Typeface loadTypeface(string file, float fontSize) @trusted {
153 
154         return new FreetypeTypeface(file, fontSize);
155 
156     }
157 
158     static Typeface loadTypeface(float fontSize) @trusted {
159 
160         return new FreetypeTypeface(fontSize);
161 
162     }
163 
164     alias loadFont = loadTypeface;
165 
166     bool opCast(T : bool)() const {
167 
168         return this !is Style(null);
169 
170     }
171 
172     bool opEquals(const Style other) const @trusted {
173 
174         // @safe: FluidBorder and Typeface are required to provide @safe opEquals.
175         // D doesn't check for opEquals on interfaces, though.
176         return this.tupleof == other.tupleof;
177 
178     }
179 
180     /// Set current DPI.
181     void setDPI(Vector2 dpi) {
182 
183         // Update the typeface
184         if (typeface) {
185 
186             typeface.dpi = dpi;
187 
188         }
189 
190         // Update the default typeface if none is assigned
191         else {
192 
193             Typeface.defaultTypeface.dpi = dpi;
194 
195         }
196 
197     }
198 
199     /// Get current typeface, or fallback to default.
200     Typeface getTypeface() {
201 
202         return either(typeface, Typeface.defaultTypeface);
203 
204     }
205 
206     const(Typeface) getTypeface() const {
207 
208         return either(typeface, Typeface.defaultTypeface);
209 
210     }
211 
212     /// Draw the background & border.
213     void drawBackground(FluidBackend backend, Rectangle rect) const {
214 
215         backend.drawRectangle(rect, backgroundColor);
216 
217         // Add border if active
218         if (borderStyle) {
219 
220             borderStyle.apply(backend, rect, border);
221 
222         }
223 
224     }
225 
226     /// Draw a line.
227     void drawLine(FluidBackend backend, Vector2 start, Vector2 end) const {
228 
229         backend.drawLine(start, end, lineColor);
230 
231     }
232 
233     /// Get a side array holding both the regular margin and the border.
234     float[4] fullMargin() const {
235 
236         return [
237             margin.sideLeft + border.sideLeft,
238             margin.sideRight + border.sideRight,
239             margin.sideTop + border.sideTop,
240             margin.sideBottom + border.sideBottom,
241         ];
242 
243     }
244 
245     /// Remove padding from the vector representing size of a box.
246     Vector2 contentBox(Vector2 size) const {
247 
248         return cropBox(size, padding);
249 
250     }
251 
252     /// Remove padding from the given rect.
253     Rectangle contentBox(Rectangle rect) const {
254 
255         return cropBox(rect, padding);
256 
257     }
258 
259     /// Get a sum of margin, border size and padding.
260     float[4] totalMargin() const {
261 
262         float[4] ret = margin[] + border[] + padding[];
263         return ret;
264 
265     }
266 
267     /// Crop the given box by reducing its size on all sides.
268     static Vector2 cropBox(Vector2 size, float[4] sides) {
269 
270         size.x = max(0, size.x - sides.sideLeft - sides.sideRight);
271         size.y = max(0, size.y - sides.sideTop - sides.sideBottom);
272 
273         return size;
274 
275     }
276 
277     /// ditto
278     static Rectangle cropBox(Rectangle rect, float[4] sides) {
279 
280         rect.x += sides.sideLeft;
281         rect.y += sides.sideTop;
282 
283         const size = cropBox(Vector2(rect.w, rect.h), sides);
284         rect.width = size.x;
285         rect.height = size.y;
286 
287         return rect;
288 
289     }
290 
291 }
292 
293 /// Side array is a static array defining a property separately for each side of a box, for example margin and border
294 /// size. Order is as follows: `[left, right, top, bottom]`. You can use `Style.Side` to index this array with an enum.
295 ///
296 /// Because of the default behavior of static arrays, one can set the value for all sides to be equal with a simple
297 /// assignment: `array = 8`. Additionally, to make it easier to manipulate the box, one may use the `sideX` and `sideY`
298 /// functions to get a `float[2]` array of the values corresponding to the given axis (which can also be assigned like
299 /// `array.sideX = 8`) or the `sideLeft`, `sideRight`, `sideTop` and `sideBottom` functions corresponding to the given
300 /// sides.
301 enum isSideArray(T) = is(T == X[4], X) && T.length == 4;
302 
303 /// ditto
304 enum isSomeSideArray(T) = isSideArray!T
305     || (is(T == Field!(name, U), string name, U) && isSideArray!U);
306 
307 ///
308 unittest {
309 
310     float[4] sides;
311     static assert(isSideArray!(float[4]));
312 
313     sides.sideX = 4;
314 
315     assert(sides.sideLeft == sides.sideRight);
316     assert(sides.sideLeft == 4);
317 
318     sides = 8;
319     assert(sides == [8, 8, 8, 8]);
320     assert(sides.sideX == sides.sideY);
321 
322 }
323 
324 /// An axis array is similar to a size array, but does not distinguish between invididual directions on a single axis.
325 /// Thus, it contains only two values, one for the X axis, and one for the Y axis.
326 ///
327 /// `sideX` and `sideY` can be used to access individual items of an axis array by name.
328 enum isAxisArray(T) = is(T == X[2], X) && T.length == 2;
329 
330 static assert(!isSideArray!(float[2]));
331 static assert( isSideArray!(float[4]));
332 
333 static assert( isAxisArray!(float[2]));
334 static assert(!isAxisArray!(float[4]));
335 
336 /// Get a reference to the left, right, top or bottom side of the given side array.
337 auto ref sideLeft(T)(return auto ref inout T sides)
338 if (isSomeSideArray!T) {
339 
340     return sides[Style.Side.left];
341 
342 }
343 
344 /// ditto
345 auto ref sideRight(T)(return auto ref inout T sides)
346 if (isSomeSideArray!T) {
347 
348     return sides[Style.Side.right];
349 
350 }
351 
352 /// ditto
353 auto ref sideTop(T)(return auto ref inout T sides)
354 if (isSomeSideArray!T) {
355 
356     return sides[Style.Side.top];
357 
358 }
359 
360 /// ditto
361 auto ref sideBottom(T)(return auto ref inout T sides)
362 if (isSomeSideArray!T) {
363 
364     return sides[Style.Side.bottom];
365 
366 }
367 
368 ///
369 unittest {
370 
371     float[4] sides = [8, 0, 4, 2];
372 
373     assert(sides.sideRight == 0);
374 
375     sides.sideRight = 8;
376     sides.sideBottom = 4;
377 
378     assert(sides == [8, 8, 4, 4]);
379 
380 }
381 
382 /// Get a reference to the X axis for the given side or axis array.
383 ref inout(ElementType!T[2]) sideX(T)(return ref inout T sides)
384 if (isSideArray!T) {
385 
386     const start = Style.Side.left;
387     return sides[start .. start + 2];
388 
389 }
390 
391 /// ditto
392 auto ref sideX(T)(return auto ref inout T sides)
393 if (isSomeSideArray!T && !isSideArray!T) {
394 
395     const start = Style.Side.left;
396     return sides[start .. start + 2];
397 
398 }
399 
400 /// ditto
401 ref inout(ElementType!T) sideX(T)(return ref inout T sides)
402 if (isAxisArray!T) {
403 
404     return sides[0];
405 
406 }
407 
408 /// Get a reference to the Y axis for the given side or axis array.
409 ref inout(ElementType!T[2]) sideY(T)(return ref inout T sides)
410 if (isSideArray!T) {
411 
412     const start = Style.Side.top;
413     return sides[start .. start + 2];
414 
415 }
416 
417 /// ditto
418 auto ref sideY(T)(return auto ref inout T sides)
419 if (isSomeSideArray!T && !isSideArray!T) {
420 
421     const start = Style.Side.top;
422     return sides[start .. start + 2];
423 
424 }
425 
426 /// ditto
427 ref inout(ElementType!T) sideY(T)(return ref inout T sides)
428 if (isAxisArray!T) {
429 
430     return sides[1];
431 
432 }
433 
434 /// Assigning values to an axis of a side array.
435 unittest {
436 
437     float[4] sides = [1, 2, 3, 4];
438 
439     assert(sides.sideX == [sides.sideLeft, sides.sideRight]);
440     assert(sides.sideY == [sides.sideTop, sides.sideBottom]);
441 
442     sides.sideX = 8;
443 
444     assert(sides == [8, 8, 3, 4]);
445 
446     sides.sideY = sides.sideBottom;
447 
448     assert(sides == [8, 8, 4, 4]);
449 
450 }
451 
452 /// Operating on an axis array.
453 @("sideX/sideY work on axis arrays")
454 unittest {
455 
456     float[2] sides;
457 
458     sides.sideX = 1;
459     sides.sideY = 2;
460 
461     assert(sides == [1, 2]);
462 
463     assert(sides.sideX == 1);
464     assert(sides.sideY == 2);
465     
466 }
467 
468 /// Returns a side array created from either: another side array like it, a two item array with each representing an
469 /// axis like `[x, y]`, or a single item array or the element type to fill all values with it.
470 T[4] normalizeSideArray(T, size_t n)(T[n] values) {
471 
472     // Already a valid side array
473     static if (n == 4) return values;
474 
475     // Axis array
476     else static if (n == 2) return [values[0], values[0], values[1], values[1]];
477 
478     // Single item array
479     else static if (n == 1) return [values[0], values[0], values[0], values[0]];
480 
481     else static assert(false, format!"Unsupported static array size %s, expected 1, 2 or 4 elements."(n));
482 
483 
484 }
485 
486 /// ditto
487 T[4] normalizeSideArray(T)(T value) {
488 
489     return [value, value, value, value];
490 
491 }
492 
493 /// Shift the side clockwise (if positive) or counter-clockwise (if negative).
494 Style.Side shiftSide(Style.Side side, int shift) {
495 
496     // Convert the side to an "angle" — 0 is the top, 1 is right and so on...
497     const angle = side.predSwitch(
498         Style.Side.top, 0,
499         Style.Side.right, 1,
500         Style.Side.bottom, 2,
501         Style.Side.left, 3,
502     );
503 
504     // Perform the shift
505     const shifted = (angle + shift) % 4;
506 
507     // And convert it back
508     return shifted.predSwitch(
509         0, Style.Side.top,
510         1, Style.Side.right,
511         2, Style.Side.bottom,
512         3, Style.Side.left,
513     );
514 
515 }
516 
517 unittest {
518 
519     assert(shiftSide(Style.Side.left, 0) == Style.Side.left);
520     assert(shiftSide(Style.Side.left, 1) == Style.Side.top);
521     assert(shiftSide(Style.Side.left, 2) == Style.Side.right);
522     assert(shiftSide(Style.Side.left, 4) == Style.Side.left);
523 
524     assert(shiftSide(Style.Side.top, 1) == Style.Side.right);
525 
526 }
527 
528 /// Make a style point the other way around
529 Style.Side reverse(Style.Side side) {
530 
531     with (Style.Side)
532     return side.predSwitch(
533         left, right,
534         right, left,
535         top, bottom,
536         bottom, top,
537     );
538 
539 }
540 
541 /// Get position of a rectangle's side, on the X axis if `left` or `right`, or on the Y axis if `top` or `bottom`.
542 float getSide(Rectangle rectangle, Style.Side side) {
543 
544     with (Style.Side)
545     return side.predSwitch(
546         left,   rectangle.x,
547         right,  rectangle.x + rectangle.width,
548         top,    rectangle.y,
549         bottom, rectangle.y + rectangle.height,
550 
551     );
552 
553 }
554 
555 unittest {
556 
557     const rect = Rectangle(0, 5, 10, 15);
558 
559     assert(rect.x == rect.getSide(Style.Side.left));
560     assert(rect.y == rect.getSide(Style.Side.top));
561     assert(rect.end.x == rect.getSide(Style.Side.right));
562     assert(rect.end.y == rect.getSide(Style.Side.bottom));
563 
564 }
565 
566 unittest {
567 
568     import fluid.frame;
569     import fluid.structs;
570 
571     auto io = new HeadlessBackend;
572     auto myTheme = nullTheme.derive(
573         rule!Frame(
574             Rule.backgroundColor = color!"fff",
575             Rule.tint = color!"aaaa",
576         ),
577     );
578     auto root = vframe(
579         layout!(1, "fill"),
580         myTheme,
581         vframe(
582             layout!(1, "fill"),
583             vframe(
584                 layout!(1, "fill"),
585                 vframe(
586                     layout!(1, "fill"),
587                 )
588             ),
589         ),
590     );
591 
592     root.io = io;
593     root.draw();
594 
595     auto rect = Rectangle(0, 0, 800, 600);
596     auto bg = color!"fff";
597 
598     // Background rectangles — all covering the same area, but with fading color and transparency
599     io.assertRectangle(rect, bg = multiply(bg, color!"aaaa"));
600     io.assertRectangle(rect, bg = multiply(bg, color!"aaaa"));
601     io.assertRectangle(rect, bg = multiply(bg, color!"aaaa"));
602     io.assertRectangle(rect, bg = multiply(bg, color!"aaaa"));
603 
604 }
605 
606 unittest {
607 
608     import fluid.frame;
609     import fluid.structs;
610 
611     auto io = new HeadlessBackend;
612     auto myTheme = nullTheme.derive(
613         rule!Frame(
614             Rule.backgroundColor = color!"fff",
615             Rule.tint = color!"aaaa",
616             Rule.border.sideRight = 1,
617             Rule.borderStyle = colorBorder(color!"f00"),
618         )
619     );
620     auto root = vframe(
621         layout!(1, "fill"),
622         myTheme,
623         vframe(
624             layout!(1, "fill"),
625             vframe(
626                 layout!(1, "fill"),
627                 vframe(
628                     layout!(1, "fill"),
629                 )
630             ),
631         ),
632     );
633 
634     root.io = io;
635     root.draw();
636 
637     auto bg = color!"fff";
638 
639     // Background rectangles — reducing in size every pixel as the border gets added
640     io.assertRectangle(Rectangle(0, 0, 800, 600), bg = multiply(bg, color!"aaaa"));
641     io.assertRectangle(Rectangle(0, 0, 799, 600), bg = multiply(bg, color!"aaaa"));
642     io.assertRectangle(Rectangle(0, 0, 798, 600), bg = multiply(bg, color!"aaaa"));
643     io.assertRectangle(Rectangle(0, 0, 797, 600), bg = multiply(bg, color!"aaaa"));
644 
645     auto border = color!"f00";
646 
647     // Border rectangles
648     io.assertRectangle(Rectangle(799, 0, 1, 600), border = multiply(border, color!"aaaa"));
649     io.assertRectangle(Rectangle(798, 0, 1, 600), border = multiply(border, color!"aaaa"));
650     io.assertRectangle(Rectangle(797, 0, 1, 600), border = multiply(border, color!"aaaa"));
651     io.assertRectangle(Rectangle(796, 0, 1, 600), border = multiply(border, color!"aaaa"));
652 
653 }