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