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 import fluid.io.canvas;
16 
17 public import fluid.theme : makeTheme, Theme, Selector, rule, Rule, when, WhenRule, children, ChildrenRule, Field,
18     Breadcrumbs;
19 public import fluid.border;
20 public import fluid.default_theme;
21 public import fluid.backend : color;
22 
23 
24 @safe:
25 
26 
27 /// Contains the style for a node.
28 struct Style {
29 
30     enum Themable;
31 
32     enum Side {
33 
34         left, right, top, bottom,
35 
36     }
37 
38     // Text options
39     @Themable {
40 
41         /// Main typeface to be used for text.
42         ///
43         /// Changing the typeface requires a resize.
44         Typeface typeface;
45 
46         alias font = typeface;
47 
48         /// Size of the font in use, in pixels.
49         ///
50         /// Changing the size requires a resize.
51         float fontSize = 14.pt;
52 
53         /// Text color.
54         auto textColor = Color(0, 0, 0, 0);
55 
56     }
57 
58     // Background & content
59     @Themable {
60 
61         /// Color of lines belonging to the node, especially important to separators and sliders.
62         auto lineColor = Color(0, 0, 0, 0);
63 
64         /// Background color of the node.
65         auto backgroundColor = Color(0, 0, 0, 0);
66 
67         /// Background color for selected text.
68         auto selectionBackgroundColor = Color(0, 0, 0, 0);
69 
70     }
71 
72     // Spacing
73     @Themable {
74 
75         /// Margin (outer margin) of the node. `[left, right, top, bottom]`.
76         ///
77         /// Updating margins requires a resize.
78         ///
79         /// See: `isSideArray`.
80         float[4] margin = 0;
81 
82         /// Border size, placed between margin and padding. `[left, right, top, bottom]`.
83         ///
84         /// Updating border requires a resize.
85         ///
86         /// See: `isSideArray`
87         float[4] border = 0;
88 
89         /// Padding (inner margin) of the node. `[left, right, top, bottom]`.
90         ///
91         /// Updating padding requires a resize.
92         ///
93         /// See: `isSideArray`
94         float[4] padding = 0;
95 
96         /// Margin/gap between two neighboring elements; for container nodes that support it.
97         ///
98         /// Updating the gap requires a resize.
99         float[2] gap = 0;
100 
101         /// Border style to use.
102         ///
103         /// Updating border requires a resize.
104         FluidBorder borderStyle;
105 
106     }
107 
108     // Misc
109     public {
110 
111         /// Apply tint to all node contents, including children.
112         @Themable
113         Color tint = Color(0xff, 0xff, 0xff, 0xff);
114 
115         /// Cursor icon to use while this node is hovered.
116         ///
117         /// Custom image cursors are not supported yet.
118         @Themable
119         FluidMouseCursor mouseCursor;
120 
121         /// Additional information for the node the style applies to.
122         ///
123         /// Ignored if mismatched.
124         @Themable
125         Node.Extra extra;
126 
127         /// Get or set node opacity. Value in range [0, 1] — 0 is fully transparent, 1 is fully opaque.
128         float opacity() const {
129 
130             return tint.a / 255.0f;
131 
132         }
133 
134         /// ditto
135         float opacity(float value) {
136 
137             tint.a = cast(ubyte) clamp(value * ubyte.max, ubyte.min, ubyte.max);
138 
139             return value;
140 
141         }
142 
143     }
144 
145     public {
146 
147         /// Breadcrumbs associated with this style. Used to keep track of tree-aware theme selectors, such as
148         /// `children`. Does not include breadcrumbs loaded by parent nodes.
149         Breadcrumbs breadcrumbs;
150 
151     }
152 
153     private this(Typeface typeface) {
154 
155         this.typeface = typeface;
156 
157     }
158 
159     static Typeface defaultTypeface() {
160 
161         return Typeface.defaultTypeface;
162 
163     }
164 
165     static Typeface loadTypeface(string file) {
166 
167         return new FreetypeTypeface(file);
168 
169     }
170 
171     alias loadFont = loadTypeface;
172 
173     bool opCast(T : bool)() const {
174 
175         return this !is Style(null);
176 
177     }
178 
179     bool opEquals(const Style other) const @trusted {
180 
181         // @safe: FluidBorder and Typeface are required to provide @safe opEquals.
182         // D doesn't check for opEquals on interfaces, though.
183         return this.tupleof == other.tupleof;
184 
185     }
186 
187     /// Set current DPI.
188     void setDPI(Vector2 dpi) {
189 
190         getTypeface.setSize(dpi, fontSize);
191 
192     }
193 
194     /// Get current typeface, or fallback to default.
195     Typeface getTypeface() {
196 
197         return either(typeface, Typeface.defaultTypeface);
198 
199     }
200 
201     const(Typeface) getTypeface() const {
202 
203         return either(typeface, Typeface.defaultTypeface);
204 
205     }
206 
207     /// Draw the background & border.
208     void drawBackground(FluidBackend backend, Rectangle rect) const {
209 
210         backend.drawRectangle(rect, backgroundColor);
211 
212         // Add border if active
213         if (borderStyle) {
214 
215             borderStyle.apply(backend, rect, border);
216 
217         }
218 
219     }
220 
221     /// ditto
222     void drawBackground(FluidBackend backend, CanvasIO io, Rectangle rect) const {
223 
224         // New I/O system used
225         if (io) {
226 
227             const ioBorder = cast(const FluidIOBorder) borderStyle;
228 
229             io.drawRectangle(rect, backgroundColor);
230 
231             // Draw border if present and compatible
232             if (ioBorder) {
233                 ioBorder.apply(io, rect, border);
234             }
235 
236         }
237 
238         // Old Backend system
239         else drawBackground(backend, rect);
240 
241     }
242 
243     /// Draw a line.
244     void drawLine(FluidBackend backend, Vector2 start, Vector2 end) const {
245 
246         backend.drawLine(start, end, lineColor);
247 
248     }
249 
250     /// ditto
251     void drawLine(FluidBackend backend, CanvasIO canvasIO, Vector2 start, Vector2 end) const {
252 
253         // New I/O system used
254         if (canvasIO) {
255 
256             canvasIO.drawLine(start, end, 1, lineColor);
257 
258         }
259 
260         else drawLine(backend, start, end);
261 
262     }
263 
264     /// Get a side array holding both the regular margin and the border.
265     float[4] fullMargin() const {
266 
267         return [
268             margin.sideLeft + border.sideLeft,
269             margin.sideRight + border.sideRight,
270             margin.sideTop + border.sideTop,
271             margin.sideBottom + border.sideBottom,
272         ];
273 
274     }
275 
276     /// Remove padding from the vector representing size of a box.
277     Vector2 contentBox(Vector2 size) const {
278 
279         return cropBox(size, padding);
280 
281     }
282 
283     /// Remove padding from the given rect.
284     Rectangle contentBox(Rectangle rect) const {
285 
286         return cropBox(rect, padding);
287 
288     }
289 
290     /// Get a sum of margin, border size and padding.
291     float[4] totalMargin() const {
292 
293         float[4] ret = margin[] + border[] + padding[];
294         return ret;
295 
296     }
297 
298     /// Crop the given box by reducing its size on all sides.
299     static Vector2 cropBox(Vector2 size, float[4] sides) {
300 
301         size.x = max(0, size.x - sides.sideLeft - sides.sideRight);
302         size.y = max(0, size.y - sides.sideTop - sides.sideBottom);
303 
304         return size;
305 
306     }
307 
308     /// ditto
309     static Rectangle cropBox(Rectangle rect, float[4] sides) {
310 
311         rect.x += sides.sideLeft;
312         rect.y += sides.sideTop;
313 
314         const size = cropBox(Vector2(rect.w, rect.h), sides);
315         rect.width = size.x;
316         rect.height = size.y;
317 
318         return rect;
319 
320     }
321 
322 }
323 
324 /// Side array is a static array defining a property separately for each side of a box, for example margin and border
325 /// size. Order is as follows: `[left, right, top, bottom]`. You can use `Style.Side` to index this array with an enum.
326 ///
327 /// Because of the default behavior of static arrays, one can set the value for all sides to be equal with a simple
328 /// assignment: `array = 8`. Additionally, to make it easier to manipulate the box, one may use the `sideX` and `sideY`
329 /// functions to get a `float[2]` array of the values corresponding to the given axis (which can also be assigned like
330 /// `array.sideX = 8`) or the `sideLeft`, `sideRight`, `sideTop` and `sideBottom` functions corresponding to the given
331 /// sides.
332 enum isSideArray(T) = is(T == X[4], X) && T.length == 4;
333 
334 /// ditto
335 enum isSomeSideArray(T) = isSideArray!T
336     || (is(T == Field!(name, U), string name, U) && isSideArray!U);
337 
338 ///
339 unittest {
340 
341     float[4] sides;
342     static assert(isSideArray!(float[4]));
343 
344     sides.sideX = 4;
345 
346     assert(sides.sideLeft == sides.sideRight);
347     assert(sides.sideLeft == 4);
348 
349     sides = 8;
350     assert(sides == [8, 8, 8, 8]);
351     assert(sides.sideX == sides.sideY);
352 
353 }
354 
355 /// An axis array is similar to a size array, but does not distinguish between invididual directions on a single axis.
356 /// Thus, it contains only two values, one for the X axis, and one for the Y axis.
357 ///
358 /// `sideX` and `sideY` can be used to access individual items of an axis array by name.
359 enum isAxisArray(T) = is(T == X[2], X) && T.length == 2;
360 
361 static assert(!isSideArray!(float[2]));
362 static assert( isSideArray!(float[4]));
363 
364 static assert( isAxisArray!(float[2]));
365 static assert(!isAxisArray!(float[4]));
366 
367 /// Get a reference to the left, right, top or bottom side of the given side array.
368 auto ref sideLeft(T)(return auto ref inout T sides)
369 if (isSomeSideArray!T) {
370 
371     return sides[Style.Side.left];
372 
373 }
374 
375 /// ditto
376 auto ref sideRight(T)(return auto ref inout T sides)
377 if (isSomeSideArray!T) {
378 
379     return sides[Style.Side.right];
380 
381 }
382 
383 /// ditto
384 auto ref sideTop(T)(return auto ref inout T sides)
385 if (isSomeSideArray!T) {
386 
387     return sides[Style.Side.top];
388 
389 }
390 
391 /// ditto
392 auto ref sideBottom(T)(return auto ref inout T sides)
393 if (isSomeSideArray!T) {
394 
395     return sides[Style.Side.bottom];
396 
397 }
398 
399 ///
400 unittest {
401 
402     float[4] sides = [8, 0, 4, 2];
403 
404     assert(sides.sideRight == 0);
405 
406     sides.sideRight = 8;
407     sides.sideBottom = 4;
408 
409     assert(sides == [8, 8, 4, 4]);
410 
411 }
412 
413 /// Get a reference to the X axis for the given side or axis array.
414 ref inout(ElementType!T[2]) sideX(T)(return ref inout T sides)
415 if (isSideArray!T) {
416 
417     const start = Style.Side.left;
418     return sides[start .. start + 2];
419 
420 }
421 
422 /// ditto
423 auto ref sideX(T)(return auto ref inout T sides)
424 if (isSomeSideArray!T && !isSideArray!T) {
425 
426     const start = Style.Side.left;
427     return sides[start .. start + 2];
428 
429 }
430 
431 /// ditto
432 ref inout(ElementType!T) sideX(T)(return ref inout T sides)
433 if (isAxisArray!T) {
434 
435     return sides[0];
436 
437 }
438 
439 /// Get a reference to the Y axis for the given side or axis array.
440 ref inout(ElementType!T[2]) sideY(T)(return ref inout T sides)
441 if (isSideArray!T) {
442 
443     const start = Style.Side.top;
444     return sides[start .. start + 2];
445 
446 }
447 
448 /// ditto
449 auto ref sideY(T)(return auto ref inout T sides)
450 if (isSomeSideArray!T && !isSideArray!T) {
451 
452     const start = Style.Side.top;
453     return sides[start .. start + 2];
454 
455 }
456 
457 /// ditto
458 ref inout(ElementType!T) sideY(T)(return ref inout T sides)
459 if (isAxisArray!T) {
460 
461     return sides[1];
462 
463 }
464 
465 /// Assigning values to an axis of a side array.
466 unittest {
467 
468     float[4] sides = [1, 2, 3, 4];
469 
470     assert(sides.sideX == [sides.sideLeft, sides.sideRight]);
471     assert(sides.sideY == [sides.sideTop, sides.sideBottom]);
472 
473     sides.sideX = 8;
474 
475     assert(sides == [8, 8, 3, 4]);
476 
477     sides.sideY = sides.sideBottom;
478 
479     assert(sides == [8, 8, 4, 4]);
480 
481 }
482 
483 /// Operating on an axis array.
484 @("sideX/sideY work on axis arrays")
485 unittest {
486 
487     float[2] sides;
488 
489     sides.sideX = 1;
490     sides.sideY = 2;
491 
492     assert(sides == [1, 2]);
493 
494     assert(sides.sideX == 1);
495     assert(sides.sideY == 2);
496 
497 }
498 
499 /// Returns a side array created from either: another side array like it, a two item array with each representing an
500 /// axis like `[x, y]`, or a single item array or the element type to fill all values with it.
501 T[4] normalizeSideArray(T, size_t n)(T[n] values) {
502 
503     // Already a valid side array
504     static if (n == 4) return values;
505 
506     // Axis array
507     else static if (n == 2) return [values[0], values[0], values[1], values[1]];
508 
509     // Single item array
510     else static if (n == 1) return [values[0], values[0], values[0], values[0]];
511 
512     else static assert(false, format!"Unsupported static array size %s, expected 1, 2 or 4 elements."(n));
513 
514 
515 }
516 
517 /// ditto
518 T[4] normalizeSideArray(T)(T value) {
519 
520     return [value, value, value, value];
521 
522 }
523 
524 /// Shift the side clockwise (if positive) or counter-clockwise (if negative).
525 Style.Side shiftSide(Style.Side side, int shift) {
526 
527     // Convert the side to an "angle" — 0 is the top, 1 is right and so on...
528     const angle = side.predSwitch(
529         Style.Side.top, 0,
530         Style.Side.right, 1,
531         Style.Side.bottom, 2,
532         Style.Side.left, 3,
533     );
534 
535     // Perform the shift
536     const shifted = (angle + shift) % 4;
537 
538     // And convert it back
539     return shifted.predSwitch(
540         0, Style.Side.top,
541         1, Style.Side.right,
542         2, Style.Side.bottom,
543         3, Style.Side.left,
544     );
545 
546 }
547 
548 unittest {
549 
550     assert(shiftSide(Style.Side.left, 0) == Style.Side.left);
551     assert(shiftSide(Style.Side.left, 1) == Style.Side.top);
552     assert(shiftSide(Style.Side.left, 2) == Style.Side.right);
553     assert(shiftSide(Style.Side.left, 4) == Style.Side.left);
554 
555     assert(shiftSide(Style.Side.top, 1) == Style.Side.right);
556 
557 }
558 
559 /// Make a style point the other way around
560 Style.Side reverse(Style.Side side) {
561 
562     with (Style.Side)
563     return side.predSwitch(
564         left, right,
565         right, left,
566         top, bottom,
567         bottom, top,
568     );
569 
570 }
571 
572 /// Get position of a rectangle's side, on the X axis if `left` or `right`, or on the Y axis if `top` or `bottom`.
573 float getSide(Rectangle rectangle, Style.Side side) {
574 
575     with (Style.Side)
576     return side.predSwitch(
577         left,   rectangle.x,
578         right,  rectangle.x + rectangle.width,
579         top,    rectangle.y,
580         bottom, rectangle.y + rectangle.height,
581 
582     );
583 
584 }
585 
586 unittest {
587 
588     const rect = Rectangle(0, 5, 10, 15);
589 
590     assert(rect.x == rect.getSide(Style.Side.left));
591     assert(rect.y == rect.getSide(Style.Side.top));
592     assert(rect.end.x == rect.getSide(Style.Side.right));
593     assert(rect.end.y == rect.getSide(Style.Side.bottom));
594 
595 }
596 
597 @("Legacy: Style.tint stacks (migrated)")
598 unittest {
599 
600     import fluid.frame;
601     import fluid.structs;
602 
603     auto io = new HeadlessBackend;
604     auto myTheme = nullTheme.derive(
605         rule!Frame(
606             Rule.backgroundColor = color!"fff",
607             Rule.tint = color!"aaaa",
608         ),
609     );
610     auto root = vframe(
611         layout!(1, "fill"),
612         myTheme,
613         vframe(
614             layout!(1, "fill"),
615             vframe(
616                 layout!(1, "fill"),
617                 vframe(
618                     layout!(1, "fill"),
619                 )
620             ),
621         ),
622     );
623 
624     root.io = io;
625     root.draw();
626 
627     auto rect = Rectangle(0, 0, 800, 600);
628     auto bg = color!"fff";
629 
630     // Background rectangles — all covering the same area, but with fading color and transparency
631     io.assertRectangle(rect, bg = multiply(bg, color!"aaaa"));
632     io.assertRectangle(rect, bg = multiply(bg, color!"aaaa"));
633     io.assertRectangle(rect, bg = multiply(bg, color!"aaaa"));
634     io.assertRectangle(rect, bg = multiply(bg, color!"aaaa"));
635 
636 }
637 
638 @("Legacy: Border occupies and takes space (abandoned)")
639 unittest {
640 
641     import fluid.frame;
642     import fluid.structs;
643 
644     auto io = new HeadlessBackend;
645     auto myTheme = nullTheme.derive(
646         rule!Frame(
647             Rule.backgroundColor = color!"fff",
648             Rule.tint = color!"aaaa",
649             Rule.border.sideRight = 1,
650             Rule.borderStyle = colorBorder(color!"f00"),
651         )
652     );
653     auto root = vframe(
654         layout!(1, "fill"),
655         myTheme,
656         vframe(
657             layout!(1, "fill"),
658             vframe(
659                 layout!(1, "fill"),
660                 vframe(
661                     layout!(1, "fill"),
662                 )
663             ),
664         ),
665     );
666 
667     root.io = io;
668     root.draw();
669 
670     auto bg = color!"fff";
671 
672     // Background rectangles — reducing in size every pixel as the border gets added
673     io.assertRectangle(Rectangle(0, 0, 800, 600), bg = multiply(bg, color!"aaaa"));
674     io.assertRectangle(Rectangle(0, 0, 799, 600), bg = multiply(bg, color!"aaaa"));
675     io.assertRectangle(Rectangle(0, 0, 798, 600), bg = multiply(bg, color!"aaaa"));
676     io.assertRectangle(Rectangle(0, 0, 797, 600), bg = multiply(bg, color!"aaaa"));
677 
678     auto border = color!"f00";
679 
680     // Border rectangles
681     io.assertRectangle(Rectangle(799, 0, 1, 600), border = multiply(border, color!"aaaa"));
682     io.assertRectangle(Rectangle(798, 0, 1, 600), border = multiply(border, color!"aaaa"));
683     io.assertRectangle(Rectangle(797, 0, 1, 600), border = multiply(border, color!"aaaa"));
684     io.assertRectangle(Rectangle(796, 0, 1, 600), border = multiply(border, color!"aaaa"));
685 
686 }
687 
688 /// Check if a rectangle is located above (`isAbove`), below (`isBelow`), to the left (`isToLeft`) or to the right
689 /// (`isToRight`) of another rectangle.
690 ///
691 /// The four functions wrap `isBeyond` which accepts a `Side` argument to specify direction at runtime.
692 ///
693 /// Params:
694 ///     subject   = Rectangle subject to the query. "Is *this* rectangle above the other?"
695 ///     reference = Rectangle used as reference.
696 ///     side      = If using `isBeyond`, the direction the subject is expected to be in relation to the other.
697 bool isAbove(Rectangle subject, Rectangle reference) {
698     return isBeyond(subject, reference, Style.Side.top);
699 }
700 
701 /// ditto
702 bool isBelow(Rectangle subject, Rectangle reference) {
703     return isBeyond(subject, reference, Style.Side.bottom);
704 }
705 
706 /// ditto
707 bool isToLeft(Rectangle subject, Rectangle reference) {
708     return isBeyond(subject, reference, Style.Side.left);
709 }
710 
711 /// ditto
712 bool isToRight(Rectangle subject, Rectangle reference) {
713     return isBeyond(subject, reference, Style.Side.right);
714 }
715 
716 /// ditto
717 bool isBeyond(Rectangle subject, Rectangle reference, Style.Side side) {
718 
719     // Distance between box sides facing each other.
720     // To illustrate, we're checking if the subject is to the right of the reference box:
721     // (side = right, side.reverse = left)
722     //
723     // ↓ reference     ↓ subject
724     // +------+        +======+
725     // |      |        |      |
726     // |      | ~~~~~~ |      |
727     // |      |        |      |
728     // +------+        +======+
729     //   side ↑        ↑ side.reverse
730     const distanceExternal = reference.getSide(side) - subject.getSide(side.reverse);
731 
732     // Distance between corresponding box sides.
733     //
734     // ↓ reference     ↓ subject
735     // +------+        +======+
736     // |      |        :      |
737     // |      | ~~~~~~~~~~~~~ |
738     // |      |        :      |
739     // +------+        +======+
740     //   side ↑          side ↑
741     const distanceInternal = reference.getSide(side) - subject.getSide(side);
742 
743     // The condition for the return value to be true, is for distanceInternal to be greater than distanceExternal.
744     // This is not the case in the opposite situation.
745     //
746     // For example, if we're checking if the subject is on the *right* of reference:
747     //
748     // trueish scenario:                                 falseish scenario:
749     // Subject is to the right of reference              Subject is the left of reference
750     //
751     // ↓ reference     ↓ subject                         ↓ subject       ↓ reference
752     // +------+        +======+                          +======+        +------+
753     // |      | ~~~~~~ :      | external                 | ~~~~~~~~~~~~~~~~~~~~ | external
754     // |      |        :      |    <                     |      :        :      |    >
755     // |      | ~~~~~~~~~~~~~ | internal                 |      : ~~~~~~~~~~~~~ | internal
756     // +------+        +======+                          +======+        +------+
757     //   side ↑        ↑ side.reverse                      side ↑          side ↑
758     const condition = abs(distanceInternal) > abs(distanceExternal);
759 
760     // ↓ subject                There is an edgecase though. If one box entirely overlaps the other on one axis,
761     // +====================+   it will be simultaneously to the left, and to the right, creating an ambiguity.
762     // |   ↓ reference      |
763     // |   +------------+   |   This is unwated in scenarios like focus switching. A scrollbar placed to the right
764     // |   |            |   |   of the page, should be focused by the right key, not by up or down.
765     // +===|            |===+
766     //     |            |       For this reason, we require both `distanceInternal` and `distanceExternal` to have
767     //     +------------+       the same sign, as it normally would, but not in case of an overlap.
768     return condition
769         && distanceInternal * distanceExternal >= 0;
770 
771 }
772 
773 /// Comparing two rectangles laid out in a column.
774 unittest {
775 
776     const rect1 = Rectangle(0,  0, 10, 10);
777     const rect2 = Rectangle(0, 20, 10, 10);
778 
779     assert(rect1.isAbove(rect2));
780     assert(rect2.isBelow(rect1));
781 
782     assert(!rect1.isBelow(rect2));
783     assert(!rect2.isAbove(rect1));
784     assert(!rect1.isToLeft(rect2));
785     assert(!rect2.isToLeft(rect1));
786     assert(!rect1.isToRight(rect2));
787     assert(!rect2.isToRight(rect1));
788 
789 }
790 
791 /// Comparing two rectangles laid out in a row.
792 unittest {
793 
794     const rect1 = Rectangle( 0, 0, 10, 10);
795     const rect2 = Rectangle(20, 0, 10, 10);
796 
797     assert(rect1.isToLeft(rect2));
798     assert(rect2.isToRight(rect1));
799 
800     assert(!rect1.isToRight(rect2));
801     assert(!rect2.isToLeft(rect1));
802     assert(!rect1.isAbove(rect2));
803     assert(!rect2.isAbove(rect1));
804     assert(!rect1.isBelow(rect2));
805     assert(!rect2.isBelow(rect1));
806 
807 }