1 ///
2 module glui.style;
3 
4 import raylib;
5 
6 import std.math;
7 import std.range;
8 import std.string;
9 import std.typecons;
10 import std.algorithm;
11 
12 import glui.node;
13 import glui.utils;
14 
15 public import glui.border;
16 public import glui.style_macros;
17 
18 @safe:
19 
20 /// Node theme.
21 alias StyleKeyPtr = immutable(StyleKey)*;
22 alias Theme = Style[StyleKeyPtr];
23 
24 /// Side array is a static array defining a property separately for each side of a box, for example margin and border
25 /// size. Order is as follows: `[left, right, top, bottom]`. You can use `Style.Side` to index this array with an enum.
26 ///
27 /// Because of the default behavior of static arrays, one can set the value for all sides to be equal with a simple
28 /// assignment: `array = 8`. Additionally, to make it easier to manipulate the box, one may use the `sideX` and `sideY`
29 /// functions to get a `uint[2]` array of the values corresponding to the given array (which can also be assigned like
30 /// `array.sideX = 8`) or the `sideLeft`, `sideRight`, `sideTop` and `sideBottom` functions corresponding to the given
31 /// box.
32 enum isSideArray(T) = is(T == X[4], X);
33 
34 ///
35 unittest {
36 
37     uint[4] sides;
38     static assert(isSideArray!(typeof(sides)));
39 
40     sides.sideX = 4;
41 
42     assert(sides.sideLeft == sides.sideRight);
43     assert(sides.sideLeft == 4);
44 
45     sides = 8;
46     assert(sides == [8, 8, 8, 8]);
47     assert(sides.sideX == sides.sideY);
48 
49 }
50 
51 /// An empty struct used to create unique style type identifiers.
52 struct StyleKey { }
53 
54 /// Create a new style initialized with given D code.
55 ///
56 /// raylib and std.string are accessible inside by default.
57 ///
58 /// Note: It is recommended to create a root style node defining font parameters and then inherit other styles from it.
59 ///
60 /// Params:
61 ///     init    = D code to use.
62 ///     parents = Styles to inherit from. See `Style.this` documentation for more info.
63 ///     data    = Data to pass to the code as the context. All fields of the struct will be within the style's scope.
64 Style style(string init, Data)(Data data, Style[] parents...) {
65 
66     auto result = new Style;
67 
68     with (data) with (result) mixin(init);
69 
70     return result;
71 
72 }
73 
74 /// Ditto.
75 Style style(string init)(Style[] parents...) {
76 
77     auto result = new Style(parents);
78     result.update!init;
79 
80     return result;
81 
82 }
83 
84 /// Create a color from hex code.
85 Color color(string hexCode)() {
86 
87     return color(hexCode);
88 
89 }
90 
91 /// ditto
92 Color color(string hexCode) pure {
93 
94     import std.format;
95 
96     // Remove the # if there is any
97     const hex = hexCode.chompPrefix("#");
98 
99     Color result;
100     result.a = 0xff;
101 
102     switch (hex.length) {
103 
104         // 4 digit RGBA
105         case 4:
106             formattedRead!"%x"(hex[3..4], result.a);
107             result.a *= 17;
108 
109             // Parse the rest like RGB
110             goto case;
111 
112         // 3 digit RGB
113         case 3:
114             formattedRead!"%x"(hex[0..1], result.r);
115             formattedRead!"%x"(hex[1..2], result.g);
116             formattedRead!"%x"(hex[2..3], result.b);
117             result.r *= 17;
118             result.g *= 17;
119             result.b *= 17;
120             break;
121 
122         // 8 digit RGBA
123         case 8:
124             formattedRead!"%x"(hex[6..8], result.a);
125             goto case;
126 
127         // 6 digit RGB
128         case 6:
129             formattedRead!"%x"(hex[0..2], result.r);
130             formattedRead!"%x"(hex[2..4], result.g);
131             formattedRead!"%x"(hex[4..6], result.b);
132             break;
133 
134         default:
135             assert(false, "Invalid hex code length");
136 
137     }
138 
139     return result;
140 
141 }
142 
143 unittest {
144 
145     import std.exception;
146 
147     assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff));
148     assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44));
149     assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44));
150     assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff));
151     assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0));
152 
153     assertThrown(color!"ag5");
154 
155 }
156 
157 /// Contains the style for a node.
158 class Style {
159 
160     enum Side {
161 
162         left, right, top, bottom,
163 
164     }
165 
166     // Internal use only, can't be private because it's used in mixins.
167     static {
168 
169         Theme _currentTheme;
170         Style[] _styleStack;
171 
172     }
173 
174     // Text options
175     struct {
176 
177         /// Font to be used for the text.
178         Font font;
179 
180         /// Font size (height) in pixels.
181         float fontSize;
182 
183         /// Line height, as a fraction of `fontSize`.
184         float lineHeight;
185 
186         /// Space between characters, relative to font size.
187         float charSpacing;
188 
189         /// Space between words, relative to the font size.
190         float wordSpacing;
191 
192         /// Text color.
193         Color textColor;
194 
195     }
196 
197     // Background
198     struct {
199 
200         /// Background color of the node.
201         Color backgroundColor;
202 
203     }
204 
205     // Spacing
206     struct {
207 
208         /// Margin (outer margin) of the node. `[left, right, top, bottom]`.
209         ///
210         /// See: `isSideArray`.
211         uint[4] margin;
212 
213         /// Border size, placed between margin and padding. `[left, right, top, bottom]`.
214         ///
215         /// See: `isSideArray`
216         uint[4] border;
217 
218         /// Padding (inner margin) of the node. `[left, right, top, bottom]`.
219         ///
220         /// See: `isSideArray`
221         uint[4] padding;
222 
223         /// Border style to use.
224         GluiBorder borderStyle;
225 
226     }
227 
228     // Misc
229     struct {
230 
231         /// Cursor icon to use while this node is hovered.
232         ///
233         /// Custom image cursors are not supported yet.
234         MouseCursor mouseCursor;
235 
236     }
237 
238     this() { }
239 
240     /// Create a style by copying params of others.
241     ///
242     /// Multiple styles can be set, so if one field is set to `typeof(field).init`, it will be taken from the previous
243     /// style from the list — that is, settings from the last style override previous ones.
244     this(Style[] styles...) {
245 
246         import std.meta, std.traits;
247 
248         // Check each style
249         foreach (i, style; styles) {
250 
251             // Inherit each field
252             static foreach (field; FieldNameTuple!(typeof(this))) {{
253 
254                 auto inheritedField = mixin("style." ~ field);
255 
256                 static if (__traits(compiles, inheritedField is null)) {
257 
258                     const isInit = inheritedField is null;
259 
260                 }
261                 else {
262 
263                     const isInit = inheritedField == inheritedField.init;
264 
265                 }
266 
267                 // Ignore if it's set to init (unless it's the first style)
268                 if (i == 0 || !isInit) {
269 
270                     mixin("this." ~ field) = inheritedField;
271 
272                 }
273 
274             }}
275 
276         }
277 
278     }
279 
280     /// Get the default, empty style.
281     static Style init() {
282 
283         static Style val;
284         if (val is null) val = new Style;
285         return val;
286 
287     }
288 
289     static Font loadFont(string file, int fontSize, dchar[] fontChars = null) @trusted {
290 
291         const realSize = fontSize * hidpiScale.y;
292 
293         return LoadFontEx(file.toStringz, cast(int) realSize.ceil, cast(int*) fontChars.ptr,
294             cast(int) fontChars.length);
295 
296     }
297 
298     /// Update the style with given D code.
299     ///
300     /// This allows each init code to have a consistent default scope, featuring `glui`, `raylib` and chosen `std`
301     /// modules.
302     ///
303     /// Params:
304     ///     init = Code to update the style with.
305     ///     T    = An compile-time object to update the scope with.
306     void update(string init)() {
307 
308         import glui;
309 
310         // Wrap init content in brackets to allow imports
311         // See: https://forum.dlang.org/thread/nl4vse$egk$1@digitalmars.com
312         // The thread mentions mixin templates but it's the same for string mixins too; and a mixin with multiple
313         // statements is annoyingly treated as multiple separate mixins.
314         mixin(init.format!"{ %s }");
315 
316     }
317 
318     /// Ditto.
319     void update(string init, T)() {
320 
321         import glui;
322 
323         with (T) mixin(init.format!"{ %s }");
324 
325     }
326 
327     /// Get the current font
328     inout(Font) getFont() inout @trusted {
329 
330         return cast(inout) (font.recs ? font : GetFontDefault);
331 
332     }
333 
334     /// Measure space given text will use.
335     ///
336     /// Params:
337     ///     availableSpace = Space available for drawing.
338     ///     text           = Text to draw.
339     ///     wrap           = If true (default), the text will be wrapped to match available space, unless the space is
340     ///                      empty.
341     /// Returns:
342     ///     If `availableSpace` is a vector, returns the result as a vector.
343     ///
344     ///     If `availableSpace` is a rectangle, returns a rectangle of the size of the result, offset to the position
345     ///     of the given rectangle.
346     Vector2 measureText(Vector2 availableSpace, string text, bool wrap = true) const
347     in (availableSpace.x.isFinite && availableSpace.y.isFinite,
348         format!"Text space given must be finite: %s"(availableSpace))
349     in (fontSize.isFinite,
350         format!"Invalid font size %s"(fontSize))
351     in (lineHeight.isFinite,
352         format!"Invalid line height %s"(lineHeight))
353     out (r; r.x.isFinite && r.y.isFinite,
354         format!"Resulting text space must be finite: %s"(r))
355     do {
356 
357         auto wrappedLines = wrapText(availableSpace.x, text, !wrap || availableSpace.x == 0);
358 
359 
360         return Vector2(
361             wrappedLines.map!"a.width".maxElement,
362             wrappedLines.length * fontSize * lineHeight,
363         );
364 
365     }
366 
367     /// Ditto
368     Rectangle measureText(Rectangle availableSpace, string text, bool wrap = true) const
369     do {
370 
371         const vec = measureText(
372             Vector2(availableSpace.width, availableSpace.height),
373             text, wrap
374         );
375 
376         return Rectangle(
377             availableSpace.x, availableSpace.y,
378             vec.x, vec.y
379         );
380 
381     }
382 
383     /// Draw text using the same params as `measureText`.
384     void drawText(Rectangle rect, string text, bool wrap = true) const {
385 
386         // Text position from top, relative to given rect
387         size_t top;
388 
389         const totalLineHeight = fontSize * lineHeight;
390 
391         // Draw each line
392         foreach (line; wrapText(rect.width, text, !wrap)) {
393 
394             scope (success) top += cast(size_t) ceil(lineHeight * fontSize);
395 
396             // Stop if drawing below rect
397             if (top > rect.height) break;
398 
399             // Text position from left
400             size_t left;
401 
402             const margin = (totalLineHeight - fontSize)/2;
403 
404             foreach (word; line.words) {
405 
406                 const position = Vector2(rect.x + left, rect.y + top + margin);
407 
408                 () @trusted {
409 
410                     // cast(): raylib doesn't mutate the font. The parameter would probably be defined `const`, but
411                     // since it's not transistive in C, and font is a struct with a pointer inside, it only matters
412                     // in D.
413 
414                     DrawTextEx(cast() getFont, word.text.toStringz, position, fontSize, fontSize * charSpacing,
415                         textColor);
416 
417                 }();
418 
419                 left += cast(size_t) ceil(word.width + fontSize * wordSpacing);
420 
421             }
422 
423         }
424 
425     }
426 
427     /// Split the text into multiple lines in order to fit within given width.
428     ///
429     /// Params:
430     ///     width         = Container width the text should fit in.
431     ///     text          = Text to wrap.
432     ///     lineFeedsOnly = If true, this should only wrap the text on line feeds.
433     TextLine[] wrapText(double width, string text, bool lineFeedsOnly) const {
434 
435         const spaceSize = cast(size_t) ceil(fontSize * wordSpacing);
436 
437         auto result = [TextLine()];
438 
439         /// Get width of the given word.
440         float wordWidth(string wordText) @trusted {
441 
442             // See drawText for cast()
443             return MeasureTextEx(cast() getFont, wordText.toStringz, fontSize, fontSize * charSpacing).x;
444 
445         }
446 
447         TextLine.Word[] words;
448 
449         auto whitespaceSplit = text[]
450             .splitter!((a, string b) => [' ', '\n'].canFind(a), Yes.keepSeparators)(" ");
451 
452         // Pass 1: split on words, calculate minimum size
453         foreach (chunk; whitespaceSplit.chunks(2)) {
454 
455             const wordText = chunk.front;
456             const size = cast(size_t) wordWidth(wordText).ceil;
457 
458             chunk.popFront;
459             const feed = chunk.empty
460                 ? false
461                 : chunk.front == "\n";
462 
463             // Push the word
464             words ~= TextLine.Word(wordText, size, feed);
465 
466             // Update minimum size
467             if (size > width) width = size;
468 
469         }
470 
471         // Pass 2: calculate total size
472         foreach (word; words) {
473 
474             scope (success) {
475 
476                 // Start a new line if this words is followed by a line feed
477                 if (word.lineFeed) result ~= TextLine();
478 
479             }
480 
481             auto lastLine = &result[$-1];
482 
483             // If last line is empty
484             if (lastLine.words == []) {
485 
486                 // Append to it immediately
487                 lastLine.words ~= word;
488                 lastLine.width += word.width;
489                 continue;
490 
491             }
492 
493 
494             // Check if this word can fit
495             if (lineFeedsOnly || lastLine.width + spaceSize + word.width <= width) {
496 
497                 // Push it to this line
498                 lastLine.words ~= word;
499                 lastLine.width += spaceSize + word.width;
500 
501             }
502 
503             // It can't
504             else {
505 
506                 // Push it to a new line
507                 result ~= TextLine([word], word.width);
508 
509             }
510 
511         }
512 
513         return result;
514 
515     }
516 
517     /// Draw the background
518     void drawBackground(Rectangle rect) const @trusted {
519 
520         DrawRectangleRec(rect, backgroundColor);
521 
522     }
523 
524     /// Get a side array holding both the regular margin and the border.
525     uint[4] fullMargin() const {
526 
527         // No border
528         if (borderStyle is null) return margin;
529 
530         return [
531             margin.sideLeft + border.sideLeft,
532             margin.sideRight + border.sideRight,
533             margin.sideTop + border.sideTop,
534             margin.sideBottom + border.sideBottom,
535         ];
536 
537     }
538 
539     /// Remove padding from the vector representing size of a box.
540     Vector2 contentBox(Vector2 size) const {
541 
542         return cropBox(size, padding);
543 
544     }
545 
546     /// Remove padding from the given rect.
547     Rectangle contentBox(Rectangle rect) const {
548 
549         return cropBox(rect, padding);
550 
551     }
552 
553     /// Get a sum of margin, border size and padding.
554     uint[4] totalMargin() const {
555 
556         uint[4] ret = margin[] + border[] + padding[];
557         return ret;
558 
559     }
560 
561     /// Crop the given box by reducing its size on all sides.
562     static Vector2 cropBox(Vector2 size, uint[4] sides) {
563 
564         size.x = max(0, size.x - sides.sideLeft - sides.sideRight);
565         size.y = max(0, size.y - sides.sideTop - sides.sideBottom);
566 
567         return size;
568 
569     }
570 
571     /// ditto
572     static Rectangle cropBox(Rectangle rect, uint[4] sides) {
573 
574         rect.x += sides.sideLeft;
575         rect.y += sides.sideTop;
576 
577         const size = cropBox(Vector2(rect.w, rect.h), sides);
578         rect.width = size.x;
579         rect.height = size.y;
580 
581         return rect;
582 
583     }
584 
585 }
586 
587 /// `wrapText` result.
588 struct TextLine {
589 
590     struct Word {
591 
592         string text;
593         size_t width;
594         bool lineFeed;  // Word is followed by a line feed.
595 
596     }
597 
598     /// Words on this line.
599     Word[] words;
600 
601     /// Width of the line (including spaces).
602     size_t width = 0;
603 
604 }
605 
606 /// Get a reference to the left, right, top or bottom side of the given side array.
607 ref inout(uint) sideLeft(T)(return ref inout T sides)
608 if (isSideArray!T) {
609 
610     return sides[Style.Side.left];
611 
612 }
613 
614 /// ditto
615 ref inout(uint) sideRight(T)(return ref inout T sides)
616 if (isSideArray!T) {
617 
618     return sides[Style.Side.right];
619 
620 }
621 
622 /// ditto
623 ref inout(uint) sideTop(T)(return ref inout T sides)
624 if (isSideArray!T) {
625 
626     return sides[Style.Side.top];
627 
628 }
629 
630 /// ditto
631 ref inout(uint) sideBottom(T)(return ref inout T sides)
632 if (isSideArray!T) {
633 
634     return sides[Style.Side.bottom];
635 
636 }
637 
638 ///
639 unittest {
640 
641     uint[4] sides = [8, 0, 4, 2];
642 
643     assert(sides.sideRight == 0);
644 
645     sides.sideRight = 8;
646     sides.sideBottom = 4;
647 
648     assert(sides == [8, 8, 4, 4]);
649 
650 }
651 
652 /// Get a reference to the X axis for the given side array.
653 ref inout(uint[2]) sideX(T)(return ref inout T sides)
654 if (isSideArray!T) {
655 
656     const start = Style.Side.left;
657     return sides[start .. start + 2];
658 
659 }
660 
661 ref inout(uint[2]) sideY(T)(return ref inout T sides)
662 if (isSideArray!T) {
663 
664     const start = Style.Side.top;
665     return sides[start .. start + 2];
666 
667 }
668 
669 ///
670 unittest {
671 
672     uint[4] sides = [1, 2, 3, 4];
673 
674     assert(sides.sideX == [sides.sideLeft, sides.sideRight]);
675     assert(sides.sideY == [sides.sideTop, sides.sideBottom]);
676 
677     sides.sideX = 8;
678 
679     assert(sides == [8, 8, 3, 4]);
680 
681     sides.sideY = sides.sideBottom;
682 
683     assert(sides == [8, 8, 4, 4]);
684 
685 }
686 
687 /// Returns a side array created from either: another side array like it, a two item array with each representing an
688 /// axis like `[x, y]`, or a single item array or the element type to fill all values with it.
689 T[4] normalizeSideArray(T, size_t n)(T[n] values) {
690 
691     // Already a valid side array
692     static if (n == 4) return values;
693 
694     // Axis array
695     else static if (n == 2) return [values[0], values[0], values[1], values[1]];
696 
697     // Single item array
698     else static if (n == 1) return [values[0], values[0], values[0], values[0]];
699 
700     else static assert(false, format!"Unsupported static array size %s, expected 1, 2 or 4 elements."(n));
701 
702 
703 }
704 
705 /// ditto
706 T[4] normalizeSideArray(T)(T value) {
707 
708     return [value, value, value, value];
709 
710 }
711 
712 /// Shift the side clockwise (if positive) or counter-clockwise (if negative).
713 Style.Side shiftSide(Style.Side side, int shift) {
714 
715     // Conver the side to an "angle" — 0 is the top, 1 is right and so on...
716     const angle = side.predSwitch(
717         Style.Side.top, 0,
718         Style.Side.right, 1,
719         Style.Side.bottom, 2,
720         Style.Side.left, 3,
721     );
722 
723     // Perform the shift
724     const shifted = (angle + shift) % 4;
725 
726     // And convert it back
727     return shifted.predSwitch(
728         0, Style.Side.top,
729         1, Style.Side.right,
730         2, Style.Side.bottom,
731         3, Style.Side.left,
732     );
733 
734 }
735 
736 unittest {
737 
738     assert(shiftSide(Style.Side.left, 0) == Style.Side.left);
739     assert(shiftSide(Style.Side.left, 1) == Style.Side.top);
740     assert(shiftSide(Style.Side.left, 2) == Style.Side.right);
741     assert(shiftSide(Style.Side.left, 4) == Style.Side.left);
742 
743     assert(shiftSide(Style.Side.top, 1) == Style.Side.right);
744 
745 }