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     static Font loadFont(string file, int fontSize, dchar[] fontChars = null) @trusted {
171 
172         const realSize = fontSize * hidpiScale.y;
173 
174         return LoadFontEx(file.toStringz, cast(int) realSize.ceil, cast(int*) fontChars.ptr,
175             cast(int) fontChars.length);
176 
177     }
178 
179     /// Update the style with given D code.
180     ///
181     /// This allows each init code to have a consistent default scope, featuring `glui`, `raylib` and chosen `std`
182     /// modules.
183     ///
184     /// Params:
185     ///     init = Code to update the style with.
186     ///     T    = An compile-time object to update the scope with.
187     void update(string init)() {
188 
189         import glui;
190 
191         // Wrap init content in brackets to allow imports
192         // See: https://forum.dlang.org/thread/nl4vse$egk$1@digitalmars.com
193         // The thread mentions mixin templates but it's the same for string mixins too; and a mixin with multiple
194         // statements is annoyingly treated as multiple separate mixins.
195         mixin(init.format!"{ %s }");
196 
197     }
198 
199     /// Ditto.
200     void update(string init, T)() {
201 
202         import glui;
203 
204         with (T) mixin(init.format!"{ %s }");
205 
206     }
207 
208     /// Get the current font
209     inout(Font) getFont() inout @trusted {
210 
211         return cast(inout) (font.recs ? font : GetFontDefault);
212 
213     }
214 
215     /// Measure space given text will use.
216     ///
217     /// Params:
218     ///     availableSpace = Space available for drawing.
219     ///     text           = Text to draw.
220     ///     wrap           = If true (default), the text will be wrapped to match available space, unless the space is
221     ///                      empty.
222     /// Returns:
223     ///     If `availableSpace` is a vector, returns the result as a vector.
224     ///
225     ///     If `availableSpace` is a rectangle, returns a rectangle of the size of the result, offset to the position
226     ///     of the given rectangle.
227     Vector2 measureText(Vector2 availableSpace, string text, bool wrap = true) const {
228 
229         auto wrapped = wrapText(availableSpace.x, text, !wrap || availableSpace.x == 0);
230 
231         return Vector2(
232             wrapped.map!"a.width".maxElement,
233             wrapped.length * fontSize * lineHeight,
234         );
235 
236     }
237 
238     /// Ditto
239     Rectangle measureText(Rectangle availableSpace, string text, bool wrap = true) const {
240 
241         const vec = measureText(
242             Vector2(availableSpace.width, availableSpace.height),
243             text, wrap
244         );
245 
246         return Rectangle(
247             availableSpace.x, availableSpace.y,
248             vec.x, vec.y
249         );
250 
251     }
252 
253     /// Draw text using the same params as `measureText`.
254     void drawText(Rectangle rect, string text, bool wrap = true) const {
255 
256         // Text position from top, relative to given rect
257         size_t top;
258 
259         const totalLineHeight = fontSize * lineHeight;
260 
261         // Draw each line
262         foreach (line; wrapText(rect.width, text, !wrap)) {
263 
264             scope (success) top += cast(size_t) ceil(lineHeight * fontSize);
265 
266             // Stop if drawing below rect
267             if (top > rect.height) break;
268 
269             // Text position from left
270             size_t left;
271 
272             const margin = (totalLineHeight - fontSize)/2;
273 
274             foreach (word; line.words) {
275 
276                 const position = Vector2(rect.x + left, rect.y + top + margin);
277 
278                 () @trusted {
279 
280                     // cast(): raylib doesn't mutate the font. The parameter would probably be defined `const`, but
281                     // since it's not transistive in C, and font is a struct with a pointer inside, it only matters
282                     // in D.
283 
284                     DrawTextEx(cast() getFont, word.text.toStringz, position, fontSize, fontSize * charSpacing,
285                         textColor);
286 
287                 }();
288 
289                 left += cast(size_t) ceil(word.width + fontSize * wordSpacing);
290 
291             }
292 
293         }
294 
295     }
296 
297     /// Split the text into multiple lines in order to fit within given width.
298     ///
299     /// Params:
300     ///     width         = Container width the text should fit in.
301     ///     text          = Text to wrap.
302     ///     lineFeedsOnly = If true, this should only wrap the text on line feeds.
303     TextLine[] wrapText(double width, string text, bool lineFeedsOnly) const {
304 
305         const spaceSize = cast(size_t) ceil(fontSize * wordSpacing);
306 
307         auto result = [TextLine()];
308 
309         /// Get width of the given word.
310         float wordWidth(string wordText) @trusted {
311 
312             // See drawText for cast()
313             return MeasureTextEx(cast() getFont, wordText.toStringz, fontSize, fontSize * charSpacing).x;
314 
315         }
316 
317         TextLine.Word[] words;
318 
319         auto whitespaceSplit = text[]
320             .splitter!((a, string b) => [' ', '\n'].canFind(a), Yes.keepSeparators)(" ");
321 
322         // Pass 1: split on words, calculate minimum size
323         foreach (chunk; whitespaceSplit.chunks(2)) {
324 
325             const wordText = chunk.front;
326             const size = cast(size_t) wordWidth(wordText).ceil;
327 
328             chunk.popFront;
329             const feed = chunk.empty
330                 ? false
331                 : chunk.front == "\n";
332 
333             // Push the word
334             words ~= TextLine.Word(wordText, size, feed);
335 
336             // Update minimum size
337             if (size > width) width = size;
338 
339         }
340 
341         // Pass 2: calculate total size
342         foreach (word; words) {
343 
344             scope (success) {
345 
346                 // Start a new line if this words is followed by a line feed
347                 if (word.lineFeed) result ~= TextLine();
348 
349             }
350 
351             auto lastLine = &result[$-1];
352 
353             // If last line is empty
354             if (lastLine.words == []) {
355 
356                 // Append to it immediately
357                 lastLine.words ~= word;
358                 lastLine.width += word.width;
359                 continue;
360 
361             }
362 
363 
364             // Check if this word can fit
365             if (lineFeedsOnly || lastLine.width + spaceSize + word.width <= width) {
366 
367                 // Push it to this line
368                 lastLine.words ~= word;
369                 lastLine.width += spaceSize + word.width;
370 
371             }
372 
373             // It can't
374             else {
375 
376                 // Push it to a new line
377                 result ~= TextLine([word], word.width);
378 
379             }
380 
381         }
382 
383         return result;
384 
385     }
386 
387     /// Draw the background
388     void drawBackground(Rectangle rect) const @trusted {
389 
390         DrawRectangleRec(rect, backgroundColor);
391 
392     }
393 
394     /// Remove padding from the vector representing size of a box.
395     Vector2 contentBox(Vector2 size) const {
396 
397         size.x = max(0, size.x - padding[0] - padding[1]);
398         size.y = max(0, size.y - padding[2] - padding[3]);
399 
400         return size;
401 
402     }
403 
404     /// Remove padding from the given rect.
405     Rectangle contentBox(Rectangle rect) const {
406 
407         rect.x += padding[0];
408         rect.y += padding[2];
409 
410         const size = contentBox(Vector2(rect.w, rect.h));
411         rect.width = size.x;
412         rect.height = size.y;
413 
414         return rect;
415 
416     }
417 
418 }
419 
420 /// `wrapText` result.
421 struct TextLine {
422 
423     struct Word {
424 
425         string text;
426         size_t width;
427         bool lineFeed;  // Word is followed by a line feed.
428 
429     }
430 
431     /// Words on this line.
432     Word[] words;
433 
434     /// Width of the line (including spaces).
435     size_t width = 0;
436 
437 }
438 
439 ref uint sideLeft(return ref uint[4] sides) {
440 
441     return sides[Style.Side.left];
442 
443 }
444 ref uint sideRight(return ref uint[4] sides) {
445 
446     return sides[Style.Side.right];
447 
448 }
449 ref uint sideTop(return ref uint[4] sides) {
450 
451     return sides[Style.Side.top];
452 
453 }
454 
455 ref uint sideBottom(return ref uint[4] sides) {
456 
457     return sides[Style.Side.bottom];
458 
459 }
460 
461 ref inout(uint[2]) sideX(return ref inout uint[4] sides) {
462 
463     const start = Style.Side.left;
464     return sides[start .. start + 2];
465 
466 }
467 
468 ref inout(uint[2]) sideY(return ref inout uint[4] sides) {
469 
470     const start = Style.Side.top;
471     return sides[start .. start + 2];
472 
473 }