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