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.utils;
13 
14 public import glui.style_macros;
15 
16 @safe:
17 
18 /// Node theme.
19 alias StyleKeyPtr = immutable(StyleKey)*;
20 alias Theme = Style[StyleKeyPtr];
21 
22 /// An empty struct used to create unique style type identifiers.
23 struct StyleKey { }
24 
25 /// Create a new style initialized with given D code.
26 ///
27 /// raylib and std.string are accessible inside by default.
28 ///
29 /// Note: It is recommended to create a root style node defining font parameters and then inherit other styles from it.
30 ///
31 /// Params:
32 ///     init    = D code to use.
33 ///     parents = Styles to inherit from. See `Style.this` documentation for more info.
34 ///     data    = Data to pass to the code as the context. All fields of the struct will be within the style's scope.
35 Style style(string init, Data)(Data data, Style[] parents...) {
36 
37     auto result = new Style;
38 
39     with (data) with (result) mixin(init);
40 
41     return result;
42 
43 }
44 
45 /// Ditto.
46 Style style(string init)(Style[] parents...) {
47 
48     auto result = new Style(parents);
49     result.update!init;
50 
51     return result;
52 
53 }
54 
55 /// Contains a style for a node.
56 class Style {
57 
58     enum Side {
59 
60         left, right, top, bottom,
61 
62     }
63 
64     // Internal use only, can't be private because it's used in mixins.
65     static {
66 
67         Theme _currentTheme;
68         Style[] _styleStack;
69 
70     }
71 
72     // Text options
73     struct {
74 
75         /// Font to be used for the text.
76         Font font;
77 
78         /// Font size (height) in pixels.
79         float fontSize;
80 
81         /// Line height, as a fraction of `fontSize`.
82         float lineHeight;
83 
84         /// Space between characters, relative to font size.
85         float charSpacing;
86 
87         /// Space between words, relative to the font size.
88         float wordSpacing;
89 
90         /// Text color.
91         Color textColor;
92 
93     }
94 
95     // Background
96     struct {
97 
98         /// Background color of the node.
99         Color backgroundColor;
100 
101     }
102 
103     // Spacing
104     struct {
105 
106         /// Margin (outer margin) of the node. `[left, right, top, bottom]`.
107         ///
108         /// Tip: You can directly set all margins with eg. `margin = 6;`
109         ///
110         /// See: enum `Side`
111         uint[4] margin;
112 
113         /// Padding (inner margin) of the node. `[left, right, top, bottom]`.
114         ///
115         /// See: enum `Side`
116         uint[4] padding;
117 
118     }
119 
120     // Misc
121     struct {
122 
123         /// Cursor icon to use while this node is hovered.
124         ///
125         /// Custom image cursors are not supported yet.
126         MouseCursor mouseCursor;
127 
128     }
129 
130     this() { }
131 
132     /// Create a style by copying params of others.
133     ///
134     /// Multiple styles can be set, so if one field is set to `typeof(field).init`, it will be taken from the previous
135     /// style from the list — that is, settings from the last style override previous ones.
136     this(Style[] styles...) {
137 
138         import std.meta, std.traits;
139 
140         // Check each style
141         foreach (i, style; styles) {
142 
143             // Inherit each field
144             static foreach (field; FieldNameTuple!(typeof(this))) {{
145 
146                 auto inheritedField = mixin("style." ~ field);
147 
148                 // Ignore if it's set to init (unless it's the first style)
149                 if (i == 0 || inheritedField != typeof(inheritedField).init) {
150 
151                     mixin("this." ~ field) = inheritedField;
152 
153                 }
154 
155             }}
156 
157         }
158 
159     }
160 
161     /// Get the default, empty style.
162     static Style init() {
163 
164         static Style val;
165         if (val is null) val = new Style;
166         return val;
167 
168     }
169 
170     /// Update the style with given D code.
171     ///
172     /// This allows each init code to have a consistent default scope, featuring `glui`, `raylib` and chosen `std`
173     /// modules.
174     ///
175     /// Params:
176     ///     init = Code to update the style with.
177     ///     T    = An compile-time object to update the scope with.
178     void update(string init)() {
179 
180         import glui;
181 
182         // Wrap init content in brackets to allow imports
183         // See: https://forum.dlang.org/thread/nl4vse$egk$1@digitalmars.com
184         // The thread mentions mixin templates but it's the same for string mixins too; and a mixin with multiple
185         // statements is annoyingly treated as multiple separate mixins.
186         mixin(init.format!"{ %s }");
187 
188     }
189 
190     /// Ditto.
191     void update(string init, T)() {
192 
193         import glui;
194 
195         with (T) mixin(init.format!"{ %s }");
196 
197     }
198 
199     /// Get the current font
200     inout(Font) getFont() inout @trusted {
201 
202         return cast(inout) (font.recs ? font : GetFontDefault);
203 
204     }
205 
206     /// Measure space given text will use.
207     ///
208     /// Params:
209     ///     availableSpace = Space available for drawing.
210     ///     text           = Text to draw.
211     ///     wrap           = If true (default), the text will be wrapped to match available space, unless the space is
212     ///                      empty.
213     /// Returns:
214     ///     If `availableSpace` is a vector, returns the result as a vector.
215     ///
216     ///     If `availableSpace` is a rectangle, returns a rectangle of the size of the result, offset to the position
217     ///     of the given rectangle.
218     Vector2 measureText(Vector2 availableSpace, string text, bool wrap = true) const {
219 
220         auto wrapped = wrapText(availableSpace.x, text, !wrap || availableSpace.x == 0);
221 
222         return Vector2(
223             wrapped.map!"a.width".maxElement,
224             wrapped.length * fontSize * lineHeight,
225         );
226 
227     }
228 
229     /// Ditto
230     Rectangle measureText(Rectangle availableSpace, string text, bool wrap = true) const {
231 
232         const vec = measureText(
233             Vector2(availableSpace.width, availableSpace.height),
234             text, wrap
235         );
236 
237         return Rectangle(
238             availableSpace.x, availableSpace.y,
239             vec.x, vec.y
240         );
241 
242     }
243 
244     /// Draw text using the same params as `measureText`.
245     void drawText(Rectangle rect, string text, bool wrap = true) const {
246 
247         // Text position from top, relative to given rect
248         size_t top;
249 
250         const totalLineHeight = fontSize * lineHeight;
251 
252         // Draw each line
253         foreach (line; wrapText(rect.width, text, !wrap)) {
254 
255             scope (success) top += cast(size_t) ceil(lineHeight * fontSize);
256 
257             // Stop if drawing below rect
258             if (top > rect.height) break;
259 
260             // Text position from left
261             size_t left;
262 
263             const margin = (totalLineHeight - fontSize)/2;
264 
265             foreach (word; line.words) {
266 
267                 const position = Vector2(rect.x + left, rect.y + top + margin);
268 
269                 () @trusted {
270 
271                     // cast(): raylib doesn't mutate the font. The parameter would probably be defined `const`, but
272                     // since it's not transistive in C, and font is a struct with a pointer inside, it only matters
273                     // in D.
274 
275                     DrawTextEx(cast() getFont, word.text.toStringz, position, fontSize, fontSize * charSpacing,
276                         textColor);
277 
278                 }();
279 
280                 left += cast(size_t) ceil(word.width + fontSize * wordSpacing);
281 
282             }
283 
284         }
285 
286     }
287 
288     /// Split the text into multiple lines in order to fit within given width.
289     ///
290     /// Params:
291     ///     width         = Container width the text should fit in.
292     ///     text          = Text to wrap.
293     ///     lineFeedsOnly = If true, this should only wrap the text on line feeds.
294     TextLine[] wrapText(double width, string text, bool lineFeedsOnly) const {
295 
296         const spaceSize = cast(size_t) ceil(fontSize * wordSpacing);
297 
298         auto result = [TextLine()];
299 
300         /// Get width of the given word.
301         float wordWidth(string wordText) @trusted {
302 
303             // See drawText for cast()
304             return MeasureTextEx(cast() getFont, wordText.toStringz, fontSize, fontSize * charSpacing).x;
305 
306         }
307 
308         TextLine.Word[] words;
309 
310         auto whitespaceSplit = text[]
311             .splitter!((a, string b) => [' ', '\n'].canFind(a), Yes.keepSeparators)(" ");
312 
313         // Pass 1: split on words, calculate minimum size
314         foreach (chunk; whitespaceSplit.chunks(2)) {
315 
316             const wordText = chunk.front;
317             const size = cast(size_t) wordWidth(wordText).ceil;
318 
319             chunk.popFront;
320             const feed = chunk.empty
321                 ? false
322                 : chunk.front == "\n";
323 
324             // Push the word
325             words ~= TextLine.Word(wordText, size, feed);
326 
327             // Update minimum size
328             if (size > width) width = size;
329 
330         }
331 
332         // Pass 2: calculate total size
333         foreach (word; words) {
334 
335             scope (success) {
336 
337                 // Start a new line if this words is followed by a line feed
338                 if (word.lineFeed) result ~= TextLine();
339 
340             }
341 
342             auto lastLine = &result[$-1];
343 
344             // If last line is empty
345             if (lastLine.words == []) {
346 
347                 // Append to it immediately
348                 lastLine.words ~= word;
349                 lastLine.width += word.width;
350                 continue;
351 
352             }
353 
354 
355             // Check if this word can fit
356             if (lineFeedsOnly || lastLine.width + spaceSize + word.width <= width) {
357 
358                 // Push it to this line
359                 lastLine.words ~= word;
360                 lastLine.width += spaceSize + word.width;
361 
362             }
363 
364             // It can't
365             else {
366 
367                 // Push it to a new line
368                 result ~= TextLine([word], word.width);
369 
370             }
371 
372         }
373 
374         return result;
375 
376     }
377 
378     /// Draw the background
379     void drawBackground(Rectangle rect) const @trusted {
380 
381         DrawRectangleRec(rect, backgroundColor);
382 
383     }
384 
385     /// Remove padding from the vector representing size of a box.
386     Vector2 contentBox(Vector2 size) const {
387 
388         size.x = max(0, size.x - padding[0] - padding[1]);
389         size.y = max(0, size.y - padding[2] - padding[3]);
390 
391         return size;
392 
393     }
394 
395     /// Remove padding from the given rect.
396     Rectangle contentBox(Rectangle rect) const {
397 
398         rect.x += padding[0];
399         rect.y += padding[2];
400 
401         const size = contentBox(Vector2(rect.w, rect.h));
402         rect.width = size.x;
403         rect.height = size.y;
404 
405         return rect;
406 
407     }
408 
409 }
410 
411 /// `wrapText` result.
412 struct TextLine {
413 
414     struct Word {
415 
416         string text;
417         size_t width;
418         bool lineFeed;  // Word is followed by a line feed.
419 
420     }
421 
422     /// Words on this line.
423     Word[] words;
424 
425     /// Width of the line (including spaces).
426     size_t width = 0;
427 
428 }
429 
430 ref uint sideLeft(return ref uint[4] sides) {
431 
432     return sides[Style.Side.left];
433 
434 }
435 ref uint sideRight(return ref uint[4] sides) {
436 
437     return sides[Style.Side.right];
438 
439 }
440 ref uint sideTop(return ref uint[4] sides) {
441 
442     return sides[Style.Side.top];
443 
444 }
445 
446 ref uint sideBottom(return ref uint[4] sides) {
447 
448     return sides[Style.Side.bottom];
449 
450 }
451 
452 ref uint[2] sideX(return ref uint[4] sides) {
453 
454     const start = Style.Side.left;
455     return sides[start .. start + 2];
456 
457 }
458 
459 ref uint[2] sideY(return ref uint[4] sides) {
460 
461     const start = Style.Side.top;
462     return sides[start .. start + 2];
463 
464 }