1 ///
2 module glui.rich_label;
3 
4 import raylib;
5 
6 import std.conv;
7 import std.meta;
8 import std.array;
9 import std.typecons;
10 import std.algorithm;
11 
12 import glui.node;
13 import glui.utils;
14 import glui.style;
15 
16 alias richLabel = simpleConstructor!GluiRichLabel;
17 
18 @safe:
19 
20 /// Defines a part of the label text.
21 struct Part {
22 
23     /// Style to apply for this part. If null, uses default style instead.
24     Rebindable!(const Style) style;
25 
26     /// Text for this part.
27     string text;
28 
29 }
30 
31 enum isPart(T) = is(T : Style) || is(T == string);
32 
33 /// A rich label can display text on the screen and apply custom styling to parts of the text.
34 ///
35 /// Warning: This component is currently difficult to use and easy to break. It is also lacking in features such as
36 /// wrapping. To obtain styles in order to pass to it, it's recommended to create a custom node, define them with
37 /// `DefineStyles` and then use the resulting styles. A way to get achieve this is to subclass this node.
38 ///
39 /// Styles: $(UL
40 ///     $(LI `style` = Default style for this node.)
41 /// )
42 class GluiRichLabel : GluiNode {
43 
44     mixin DefineStyles;
45     mixin ImplHoveredRect;
46 
47     /// Parts defining label text.
48     Part[] textParts;
49 
50     static foreach (index; 0 .. BasicNodeParamLength) {
51 
52         /// Initialize the label with given text.
53         /// Params:
54         ///     content = Style objects and strings to define parts of the text.
55         this(T...)(BasicNodeParam!index sup, T content)
56         if (allSatisfy!(isPart, T)) {
57 
58             super(sup);
59 
60             foreach (elem; content) {
61 
62                 this ~= elem;
63 
64             }
65 
66         }
67 
68     }
69 
70     /// Change the style for next part of the text.
71     void opOpAssign(string op : "~")(const Style style) {
72 
73         textParts ~= Part(style.rebindable, "");
74 
75     }
76 
77     /// Append new text.
78     void opOpAssign(string op : "~", T : string)(T text)
79     if (!is(T == typeof(null))) {
80 
81         // If there is a part to append to
82         if (textParts.length) {
83 
84             textParts[$-1].text ~= text;
85 
86         }
87 
88         // Nope, make a new part
89         else textParts ~= Part(rebindable(cast(const Style) null), text);
90 
91     }
92 
93     /// Push text to the label.
94     /// Params:
95     ///     style = Style of the text.
96     ///     text  = Text to add.
97     void push(const Style style, string text) {
98 
99         textParts ~= Part(style.rebindable, text);
100 
101     }
102 
103     /// Ditto.
104     void push(string text) {
105 
106         this ~= text;
107 
108     }
109 
110     /// Get the current text of the label, as plain text.
111     string text() const {
112 
113         return textParts
114             .map!"a.text"
115             .join;
116 
117     }
118 
119     /// Erase all label contents.
120     void clear() {
121 
122         textParts = [];
123 
124     }
125 
126     protected override void resizeImpl(Vector2 available) {
127 
128         minSize = style.measureText(available, text);
129 
130     }
131 
132     protected override void drawImpl(Rectangle outer, Rectangle inner) {
133 
134         const style = pickStyle();
135         style.drawBackground(outer);
136 
137         /// Current position on the screen to append to.
138         auto cursor = Vector2(inner.x, inner.y);
139 
140         foreach (part; textParts) {
141 
142             auto text = part.text;
143 
144             while (text.length) {
145 
146                 auto current = text.until("\n", No.openRight).to!string;
147                 text = text[current.length .. $];
148 
149                 // Get area to draw in
150                 auto thisStyle = part.style is null ? style : part.style;
151                 auto area = thisStyle.measureText(
152                     Rectangle(cursor.x, cursor.y, inner.w, inner.h),
153                     current
154                 );
155                 // TODO: wrapping+indent
156 
157                 thisStyle.drawBackground(area);
158                 thisStyle.drawText(area, current);
159 
160                 // Move the cursor
161                 cursor.y += area.h - thisStyle.fontSize * thisStyle.lineHeight;
162 
163                 // Ended with a newline
164                 if (current[$-1] == '\n') cursor.x = inner.x;
165                 else cursor.x += area.w;
166 
167             }
168 
169         }
170 
171     }
172 
173     protected override const(Style) pickStyle() const {
174 
175         return style;
176 
177     }
178 
179 }