1 ///
2 module glui.text_input;
3 
4 import raylib;
5 
6 import glui.node;
7 import glui.input;
8 import glui.style;
9 import glui.utils;
10 
11 alias textInput = simpleConstructor!GluiTextInput;
12 
13 /// Raylib: Get pressed char
14 private extern (C) int GetCharPressed() nothrow @nogc;
15 
16 @safe:
17 
18 /// Text input field.
19 ///
20 /// Styles: $(UL
21 ///     $(LI `style` = Default style for the input.)
22 ///     $(LI `focusStyle` = Style for when the input is focused.)
23 ///     $(LI `emptyStyle` = Style for when the input is empty, i.e. the placeholder is visible. Text should usually be
24 ///         grayed out.)
25 /// )
26 class GluiTextInput : GluiInput!GluiNode {
27 
28     mixin DefineStyles!(
29         "emptyStyle", q{ style },
30     );
31     mixin ImplHoveredRect;
32 
33     /// Time in seconds before the cursor toggles visibility.
34     static immutable float blinkTime = 1;
35 
36     /// Size of the field.
37     auto size = Vector2(200, 0);
38 
39     /// Value of the field.
40     string value;
41 
42     /// A placeholder text for the field, displayed when the field is empty. Style using `emptyStyle`.
43     string placeholder;
44 
45     /// TODO. If true, this input accepts multiple lines.
46     bool multiline;
47 
48     static foreach (index; 0 .. BasicNodeParamLength) {
49 
50         /// Create a text input.
51         /// Params:
52         ///     sup         = Node parameters.
53         ///     placeholder = Placeholder text for the field.
54         ///     submitted   = Callback for when the field is submitted (enter pressed, ctrl+enter if multiline).
55         this(BasicNodeParam!index sup, string placeholder = "", void delegate() @trusted submitted = null) {
56 
57             super(sup);
58             this.placeholder = placeholder;
59             this.submitted = submitted;
60 
61         }
62 
63     }
64 
65     protected override void resizeImpl(Vector2 area) {
66 
67         import std.algorithm : max;
68 
69         // Set the size
70         minSize = size;
71 
72         // Single line
73         if (!multiline) {
74 
75             // Set height to at least the font size
76             minSize.y = max(minSize.y, style.fontSize * style.lineHeight);
77 
78         }
79 
80     }
81 
82     protected override void drawImpl(Rectangle outer, Rectangle inner) @trusted {
83 
84         auto style = pickStyle();
85 
86         // Fill the background
87         style.drawBackground(outer);
88 
89         // Draw the text
90         const text = (value == "") ? placeholder : value;
91 
92         // If the box is focused
93         if (isFocused) {
94 
95             import std.algorithm : max;
96 
97             const lineHeight = style.fontSize * style.lineHeight;
98 
99             auto textArea = value == ""
100                 ? Rectangle(inner.x, inner.y, 0, lineHeight)
101                 : style.measureText(inner, text, false);
102 
103             const scrollOffset = max(0, textArea.w - outer.w);
104 
105             inner.x -= scrollOffset;
106 
107             style.drawText(inner, value, false);
108 
109             // Add a blinking caret
110             if (GetTime % (blinkTime*2) < blinkTime) {
111 
112                 const margin = style.fontSize / 10f;
113                 const end = Vector2(
114                     textArea.x + textArea.width + margin,
115                     textArea.y + textArea.height,
116                 );
117 
118                 // Draw the caret
119                 DrawLineV(
120                     end - Vector2(0, lineHeight - margin),
121                     end - Vector2(0, margin),
122                     style.textColor
123                 );
124 
125             }
126 
127         }
128 
129         // Not focused, draw text
130         else style.drawText(inner, text, false);
131 
132     }
133 
134     // Do nothing, we take mouse focus while drawing.
135     protected override void mouseImpl() @trusted {
136 
137         // Update status
138         if (IsMouseButtonDown(MouseButton.MOUSE_LEFT_BUTTON)) {
139 
140             isFocused = true;
141 
142         }
143 
144     }
145 
146     protected override bool keyboardImpl() @trusted {
147 
148         import std.uni : isAlpha, isWhite;
149         import std.range : back;
150         import std.string : chop;
151 
152         bool backspace = false;
153         string input;
154 
155         // Get pressed key
156         while (true) {
157 
158             // Backspace
159             if (value.length && IsKeyPressed(KeyboardKey.KEY_BACKSPACE)) {
160 
161                 /// If true, delete whole words
162                 const word = IsKeyDown(KeyboardKey.KEY_LEFT_CONTROL);
163 
164                 // Remove the last character
165                 do {
166 
167                     const lastChar = value.back;
168                     value = value.chop;
169 
170                     backspace = true;
171 
172                     // Stop instantly if there are no characters left
173                     if (value.length == 0) break;
174 
175 
176                     // Whitespace, continue deleting
177                     if (lastChar.isWhite) continue;
178 
179                     // Matching alpha, continue deleting
180                     else if (value.back.isAlpha == lastChar.isAlpha) continue;
181 
182                     // Break in other cases
183                     break;
184 
185                 }
186 
187                 // Repeat only if requested to delete whole words
188                 while (word);
189 
190             }
191 
192             // Submit
193             if (!multiline && IsKeyPressed(KeyboardKey.KEY_ENTER)) {
194 
195                 isFocused = false;
196                 if (submitted) submitted();
197 
198                 return true;
199 
200             }
201 
202 
203             // Read text
204             if (const key = GetCharPressed()) {
205 
206                 // Append to char arrays
207                 input ~= cast(dchar) key;
208 
209             }
210 
211             // Stop if nothing left
212             else break;
213 
214         }
215 
216         value ~= input;
217 
218         // Trigger callback
219         if ((input.length || backspace) && changed) {
220 
221             changed();
222             return true;
223 
224         }
225 
226         // Even if nothing changed, user might have held the key for a while which this function probably wouldn't have
227         // caught, so we'd be returning false-positives all the time.
228         // The safest way is to just return true always, text input really is complex enough we can assume we did take
229         // any input there could be.
230         return true;
231 
232     }
233 
234     override const(Style) pickStyle() const {
235 
236         // Disabled
237         if (isDisabledInherited) return disabledStyle;
238 
239         // Focused
240         else if (isFocused) return focusStyle;
241 
242         // Empty text (display placeholder)
243         else if (value == "") return emptyStyle;
244 
245         // Other styles
246         else return super.pickStyle();
247 
248     }
249 
250 }