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.utils;
11 import fluid.backend;
12 import fluid.typeface;
13 
14 public import fluid.border;
15 public import fluid.style_macros;
16 public import fluid.default_theme;
17 public import fluid.backend : color;
18 
19 
20 @safe:
21 
22 
23 alias StyleKeyPtr = immutable(StyleKey)*;
24 
25 /// Node theme.
26 alias Theme = Style[StyleKeyPtr];
27 
28 /// Side array is a static array defining a property separately for each side of a box, for example margin and border
29 /// size. Order is as follows: `[left, right, top, bottom]`. You can use `Style.Side` to index this array with an enum.
30 ///
31 /// Because of the default behavior of static arrays, one can set the value for all sides to be equal with a simple
32 /// assignment: `array = 8`. Additionally, to make it easier to manipulate the box, one may use the `sideX` and `sideY`
33 /// functions to get a `uint[2]` array of the values corresponding to the given axis (which can also be assigned like
34 /// `array.sideX = 8`) or the `sideLeft`, `sideRight`, `sideTop` and `sideBottom` functions corresponding to the given
35 /// sides.
36 enum isSideArray(T) = is(T == X[4], X);
37 
38 ///
39 unittest {
40 
41     uint[4] sides;
42     static assert(isSideArray!(uint[4]));
43 
44     sides.sideX = 4;
45 
46     assert(sides.sideLeft == sides.sideRight);
47     assert(sides.sideLeft == 4);
48 
49     sides = 8;
50     assert(sides == [8, 8, 8, 8]);
51     assert(sides.sideX == sides.sideY);
52 
53 }
54 
55 /// An empty struct used to create unique style type identifiers.
56 struct StyleKey { }
57 
58 /// Create a new style initialized with given D code.
59 ///
60 /// raylib and std.string are accessible inside by default.
61 ///
62 /// Note: It is recommended to create a root style node defining font parameters and then inherit other styles from it.
63 ///
64 /// Params:
65 ///     init    = D code to use.
66 ///     parents = Styles to inherit from. See `Style.this` documentation for more info.
67 ///     data    = Data to pass to the code as the context. All fields of the struct will be within the style's scope.
68 Style style(string init, Data)(Data data, Style[] parents...) {
69 
70     auto result = new Style;
71 
72     with (data) with (result) mixin(init);
73 
74     return result;
75 
76 }
77 
78 /// Ditto.
79 Style style(string init)(Style[] parents...) {
80 
81     auto result = new Style(parents);
82     result.update!init;
83 
84     return result;
85 
86 }
87 
88 /// Contains the style for a node.
89 class Style {
90 
91     enum Side {
92 
93         left, right, top, bottom,
94 
95     }
96 
97     // Internal use only, can't be private because it's used in mixins.
98     static {
99 
100         Theme _currentTheme;
101         Style[] _styleStack;
102 
103     }
104 
105     // Text options
106     struct {
107 
108         /// Main typeface to be used for text.
109         Typeface typeface;
110 
111         alias font = typeface;
112 
113         deprecated("Set font parameters in the typeface. These will be removed in 0.7.0") {
114 
115             /// Font size (height) in pixels.
116             float fontSize;
117 
118             /// Line height, as a fraction of `fontSize`.
119             float lineHeight;
120 
121             /// Space between characters, relative to font size.
122             float charSpacing;
123 
124             /// Space between words, relative to the font size.
125             float wordSpacing;
126 
127         }
128 
129         /// Text color.
130         Color textColor;
131 
132     }
133 
134     // Background
135     struct {
136 
137         /// Background color of the node.
138         Color backgroundColor;
139 
140     }
141 
142     // Spacing
143     struct {
144 
145         /// Margin (outer margin) of the node. `[left, right, top, bottom]`.
146         ///
147         /// See: `isSideArray`.
148         uint[4] margin;
149 
150         /// Border size, placed between margin and padding. `[left, right, top, bottom]`.
151         ///
152         /// See: `isSideArray`
153         uint[4] border;
154 
155         /// Padding (inner margin) of the node. `[left, right, top, bottom]`.
156         ///
157         /// See: `isSideArray`
158         uint[4] padding;
159 
160         /// Border style to use.
161         FluidBorder borderStyle;
162 
163     }
164 
165     // Misc
166     struct {
167 
168         /// Cursor icon to use while this node is hovered.
169         ///
170         /// Custom image cursors are not supported yet.
171         FluidMouseCursor mouseCursor;
172 
173     }
174 
175     this() {
176 
177         this.font = Typeface.defaultTypeface;
178 
179     }
180 
181     /// Create a style by copying params of others.
182     ///
183     /// Multiple styles can be set, so if one field is set to `typeof(field).init`, it will be taken from the previous
184     /// style from the list — that is, settings from the last style override previous ones.
185     this(Style[] styles...) {
186 
187         // Check each style
188         foreach (i, style; styles) {
189 
190             // Inherit each field
191             static foreach (j; 0..this.tupleof.length) {{
192 
193                 auto inheritedField = style.tupleof[j];
194 
195                 static if (__traits(compiles, inheritedField is null)) {
196 
197                     const isInit = inheritedField is null;
198 
199                 }
200                 else {
201 
202                     const isInit = inheritedField == inheritedField.init;
203 
204                 }
205 
206                 // Ignore if it's set to init (unless it's the first style)
207                 if (i == 0 || !isInit) {
208 
209                     this.tupleof[j] = inheritedField;
210 
211                 }
212 
213             }}
214 
215         }
216 
217     }
218 
219     /// Get the default, empty style.
220     static Style init() {
221 
222         static Style val;
223         if (val is null) val = new Style;
224         return val;
225 
226     }
227 
228     static Typeface loadTypeface(string file, int fontSize) @trusted {
229 
230         return new FreetypeTypeface(file, fontSize);
231 
232     }
233 
234     static Typeface loadTypeface(int fontSize) @trusted {
235 
236         return new FreetypeTypeface(fontSize);
237 
238     }
239 
240     alias loadFont = loadTypeface;
241 
242     /// Update the style with given D code.
243     ///
244     /// This allows each init code to have a consistent default scope, featuring `fluid`, `raylib` and chosen `std`
245     /// modules.
246     ///
247     /// Params:
248     ///     init = Code to update the style with.
249     ///     T    = An compile-time object to update the scope with.
250     void update(string init)() {
251 
252         import fluid;
253 
254         // Wrap init content in brackets to allow imports
255         // See: https://forum.dlang.org/thread/nl4vse$egk$1@digitalmars.com
256         // The thread mentions mixin templates but it's the same for string mixins too; and a mixin with multiple
257         // statements is annoyingly treated as multiple separate mixins.
258         mixin(init.format!"{ %s }");
259 
260     }
261 
262     /// Ditto.
263     void update(string init, T)() {
264 
265         import fluid;
266 
267         with (T) mixin(init.format!"{ %s }");
268 
269     }
270 
271     /// Set current DPI.
272     void setDPI(Vector2 dpi) {
273 
274         // Update the typeface
275         if (typeface) {
276 
277             typeface.dpi = dpi;
278 
279         }
280 
281     }
282 
283     deprecated("Use Typeface or Text instead. To be removed in 0.7.0.") {
284 
285         /// Measure space given text will use.
286         ///
287         /// Params:
288         ///     availableSpace = Space available for drawing.
289         ///     text           = Text to draw.
290         ///     wrap           = If true (default), the text will be wrapped to match available space, unless the space is
291         ///                      empty.
292         /// Returns:
293         ///     If `availableSpace` is a vector, returns the result as a vector.
294         ///
295         ///     If `availableSpace` is a rectangle, returns a rectangle of the size of the result, offset to the position
296         ///     of the given rectangle.
297         Vector2 measureText(Vector2 availableSpace, string text, bool wrap = true) const
298         in (availableSpace.x.isFinite && availableSpace.y.isFinite,
299             format!"Text space given must be finite: %s"(availableSpace))
300         out (r; r.x.isFinite && r.y.isFinite,
301             format!"Resulting text space must be finite: %s"(r))
302         do {
303 
304             return typeface.measure(availableSpace, text, wrap);
305 
306         }
307 
308         /// Ditto
309         Rectangle measureText(Rectangle availableSpace, string text, bool wrap = true) const
310         do {
311 
312             const vec = measureText(
313                 Vector2(availableSpace.width, availableSpace.height),
314                 text, wrap
315             );
316 
317             return Rectangle(
318                 availableSpace.x, availableSpace.y,
319                 vec.x, vec.y
320             );
321 
322         }
323 
324         /// Draw text using the same params as `measureText`.
325         void drawText(ref Image image, Rectangle rect, string text, bool wrap = true) const {
326 
327             typeface.draw(image, rect, text, textColor, wrap);
328 
329         }
330 
331         /// ditto
332         void drawText(ref Image image, Rectangle rect, string text, Color color, bool wrap = true) const {
333 
334             typeface.draw(image, rect, text, color, wrap);
335 
336         }
337 
338     }
339 
340     /// Draw the background
341     void drawBackground(FluidBackend backend, Rectangle rect) const @trusted {
342 
343         backend.drawRectangle(rect, backgroundColor);
344 
345     }
346 
347     /// Get a side array holding both the regular margin and the border.
348     uint[4] fullMargin() const {
349 
350         return [
351             margin.sideLeft + border.sideLeft,
352             margin.sideRight + border.sideRight,
353             margin.sideTop + border.sideTop,
354             margin.sideBottom + border.sideBottom,
355         ];
356 
357     }
358 
359     /// Remove padding from the vector representing size of a box.
360     Vector2 contentBox(Vector2 size) const {
361 
362         return cropBox(size, padding);
363 
364     }
365 
366     /// Remove padding from the given rect.
367     Rectangle contentBox(Rectangle rect) const {
368 
369         return cropBox(rect, padding);
370 
371     }
372 
373     /// Get a sum of margin, border size and padding.
374     uint[4] totalMargin() const {
375 
376         uint[4] ret = margin[] + border[] + padding[];
377         return ret;
378 
379     }
380 
381     /// Crop the given box by reducing its size on all sides.
382     static Vector2 cropBox(Vector2 size, uint[4] sides) {
383 
384         size.x = max(0, size.x - sides.sideLeft - sides.sideRight);
385         size.y = max(0, size.y - sides.sideTop - sides.sideBottom);
386 
387         return size;
388 
389     }
390 
391     /// ditto
392     static Rectangle cropBox(Rectangle rect, uint[4] sides) {
393 
394         rect.x += sides.sideLeft;
395         rect.y += sides.sideTop;
396 
397         const size = cropBox(Vector2(rect.w, rect.h), sides);
398         rect.width = size.x;
399         rect.height = size.y;
400 
401         return rect;
402 
403     }
404 
405 }
406 
407 /// `wrapText` result.
408 struct TextLine {
409 
410     struct Word {
411 
412         string text;
413         size_t width;
414         bool lineFeed;  // Word is followed by a line feed.
415 
416     }
417 
418     /// Index of the line within the original text. This is the start of the text.
419     size_t index;
420 
421     /// Text on this line.
422     string text;
423 
424     /// Width of the line (including spaces).
425     size_t width = 0;
426 
427     /// If true, the line is explicitly terminated with a line feed.
428     bool lineFeed;
429 
430 }
431 
432 /// Get a reference to the left, right, top or bottom side of the given side array.
433 ref inout(ElementType!T) sideLeft(T)(return ref inout T sides)
434 if (isSideArray!T) {
435 
436     return sides[Style.Side.left];
437 
438 }
439 
440 /// ditto
441 ref inout(ElementType!T) sideRight(T)(return ref inout T sides)
442 if (isSideArray!T) {
443 
444     return sides[Style.Side.right];
445 
446 }
447 
448 /// ditto
449 ref inout(ElementType!T) sideTop(T)(return ref inout T sides)
450 if (isSideArray!T) {
451 
452     return sides[Style.Side.top];
453 
454 }
455 
456 /// ditto
457 ref inout(ElementType!T) sideBottom(T)(return ref inout T sides)
458 if (isSideArray!T) {
459 
460     return sides[Style.Side.bottom];
461 
462 }
463 
464 ///
465 unittest {
466 
467     uint[4] sides = [8, 0, 4, 2];
468 
469     assert(sides.sideRight == 0);
470 
471     sides.sideRight = 8;
472     sides.sideBottom = 4;
473 
474     assert(sides == [8, 8, 4, 4]);
475 
476 }
477 
478 /// Get a reference to the X axis for the given side array.
479 ref inout(uint[2]) sideX(T)(return ref inout T sides)
480 if (isSideArray!T) {
481 
482     const start = Style.Side.left;
483     return sides[start .. start + 2];
484 
485 }
486 
487 ref inout(uint[2]) sideY(T)(return ref inout T sides)
488 if (isSideArray!T) {
489 
490     const start = Style.Side.top;
491     return sides[start .. start + 2];
492 
493 }
494 
495 ///
496 unittest {
497 
498     uint[4] sides = [1, 2, 3, 4];
499 
500     assert(sides.sideX == [sides.sideLeft, sides.sideRight]);
501     assert(sides.sideY == [sides.sideTop, sides.sideBottom]);
502 
503     sides.sideX = 8;
504 
505     assert(sides == [8, 8, 3, 4]);
506 
507     sides.sideY = sides.sideBottom;
508 
509     assert(sides == [8, 8, 4, 4]);
510 
511 }
512 
513 /// Returns a side array created from either: another side array like it, a two item array with each representing an
514 /// axis like `[x, y]`, or a single item array or the element type to fill all values with it.
515 T[4] normalizeSideArray(T, size_t n)(T[n] values) {
516 
517     // Already a valid side array
518     static if (n == 4) return values;
519 
520     // Axis array
521     else static if (n == 2) return [values[0], values[0], values[1], values[1]];
522 
523     // Single item array
524     else static if (n == 1) return [values[0], values[0], values[0], values[0]];
525 
526     else static assert(false, format!"Unsupported static array size %s, expected 1, 2 or 4 elements."(n));
527 
528 
529 }
530 
531 /// ditto
532 T[4] normalizeSideArray(T)(T value) {
533 
534     return [value, value, value, value];
535 
536 }
537 
538 /// Shift the side clockwise (if positive) or counter-clockwise (if negative).
539 Style.Side shiftSide(Style.Side side, int shift) {
540 
541     // Convert the side to an "angle" — 0 is the top, 1 is right and so on...
542     const angle = side.predSwitch(
543         Style.Side.top, 0,
544         Style.Side.right, 1,
545         Style.Side.bottom, 2,
546         Style.Side.left, 3,
547     );
548 
549     // Perform the shift
550     const shifted = (angle + shift) % 4;
551 
552     // And convert it back
553     return shifted.predSwitch(
554         0, Style.Side.top,
555         1, Style.Side.right,
556         2, Style.Side.bottom,
557         3, Style.Side.left,
558     );
559 
560 }
561 
562 unittest {
563 
564     assert(shiftSide(Style.Side.left, 0) == Style.Side.left);
565     assert(shiftSide(Style.Side.left, 1) == Style.Side.top);
566     assert(shiftSide(Style.Side.left, 2) == Style.Side.right);
567     assert(shiftSide(Style.Side.left, 4) == Style.Side.left);
568 
569     assert(shiftSide(Style.Side.top, 1) == Style.Side.right);
570 
571 }
572 
573 /// Make a style point the other way around
574 Style.Side reverse(Style.Side side) {
575 
576     with (Style.Side)
577     return side.predSwitch(
578         left, right,
579         right, left,
580         top, bottom,
581         bottom, top,
582     );
583 
584 }
585 
586 /// Get position of a rectangle's side, on the X axis if `left` or `right`, or on the Y axis if `top` or `bottom`.
587 float getSide(Rectangle rectangle, Style.Side side) {
588 
589     with (Style.Side)
590     return side.predSwitch(
591         left,   rectangle.x,
592         right,  rectangle.x + rectangle.width,
593         top,    rectangle.y,
594         bottom, rectangle.y + rectangle.height,
595 
596     );
597 
598 }
599 
600 unittest {
601 
602     const rect = Rectangle(0, 5, 10, 15);
603 
604     assert(rect.x == rect.getSide(Style.Side.left));
605     assert(rect.y == rect.getSide(Style.Side.top));
606     assert(rect.end.x == rect.getSide(Style.Side.right));
607     assert(rect.end.y == rect.getSide(Style.Side.bottom));
608 
609 }