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 }